@mepuka/skygent 0.2.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 +59 -0
- package/index.ts +146 -0
- package/package.json +56 -0
- package/src/cli/app.ts +75 -0
- package/src/cli/config-command.ts +140 -0
- package/src/cli/config.ts +91 -0
- package/src/cli/derive.ts +205 -0
- package/src/cli/doc/annotation.ts +36 -0
- package/src/cli/doc/filter.ts +69 -0
- package/src/cli/doc/index.ts +9 -0
- package/src/cli/doc/post.ts +155 -0
- package/src/cli/doc/primitives.ts +25 -0
- package/src/cli/doc/render.ts +18 -0
- package/src/cli/doc/table.ts +114 -0
- package/src/cli/doc/thread.ts +46 -0
- package/src/cli/doc/tree.ts +126 -0
- package/src/cli/errors.ts +59 -0
- package/src/cli/exit-codes.ts +52 -0
- package/src/cli/feed.ts +177 -0
- package/src/cli/filter-dsl.ts +1411 -0
- package/src/cli/filter-errors.ts +208 -0
- package/src/cli/filter-help.ts +70 -0
- package/src/cli/filter-input.ts +54 -0
- package/src/cli/filter.ts +435 -0
- package/src/cli/graph.ts +472 -0
- package/src/cli/help.ts +14 -0
- package/src/cli/interval.ts +35 -0
- package/src/cli/jetstream.ts +173 -0
- package/src/cli/layers.ts +180 -0
- package/src/cli/logging.ts +136 -0
- package/src/cli/output-format.ts +26 -0
- package/src/cli/output.ts +82 -0
- package/src/cli/parse.ts +80 -0
- package/src/cli/post.ts +193 -0
- package/src/cli/preferences.ts +11 -0
- package/src/cli/query-fields.ts +247 -0
- package/src/cli/query.ts +415 -0
- package/src/cli/range.ts +44 -0
- package/src/cli/search.ts +465 -0
- package/src/cli/shared-options.ts +169 -0
- package/src/cli/shared.ts +20 -0
- package/src/cli/store-errors.ts +80 -0
- package/src/cli/store-tree.ts +392 -0
- package/src/cli/store.ts +395 -0
- package/src/cli/sync-factory.ts +107 -0
- package/src/cli/sync.ts +366 -0
- package/src/cli/view-thread.ts +196 -0
- package/src/cli/view.ts +47 -0
- package/src/cli/watch.ts +344 -0
- package/src/db/migrations/store-catalog/001_init.ts +14 -0
- package/src/db/migrations/store-index/001_init.ts +34 -0
- package/src/db/migrations/store-index/002_event_log.ts +24 -0
- package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
- package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
- package/src/db/migrations/store-index/005_post_lang.ts +15 -0
- package/src/db/migrations/store-index/006_has_embed.ts +10 -0
- package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
- package/src/domain/bsky.ts +467 -0
- package/src/domain/config.ts +11 -0
- package/src/domain/credentials.ts +6 -0
- package/src/domain/defaults.ts +8 -0
- package/src/domain/derivation.ts +55 -0
- package/src/domain/errors.ts +71 -0
- package/src/domain/events.ts +55 -0
- package/src/domain/extract.ts +64 -0
- package/src/domain/filter-describe.ts +551 -0
- package/src/domain/filter-explain.ts +9 -0
- package/src/domain/filter.ts +797 -0
- package/src/domain/format.ts +91 -0
- package/src/domain/index.ts +13 -0
- package/src/domain/indexes.ts +17 -0
- package/src/domain/policies.ts +16 -0
- package/src/domain/post.ts +88 -0
- package/src/domain/primitives.ts +50 -0
- package/src/domain/raw.ts +140 -0
- package/src/domain/store.ts +103 -0
- package/src/domain/sync.ts +211 -0
- package/src/domain/text-width.ts +56 -0
- package/src/services/app-config.ts +278 -0
- package/src/services/bsky-client.ts +2113 -0
- package/src/services/credential-store.ts +408 -0
- package/src/services/derivation-engine.ts +502 -0
- package/src/services/derivation-settings.ts +61 -0
- package/src/services/derivation-validator.ts +68 -0
- package/src/services/filter-compiler.ts +269 -0
- package/src/services/filter-library.ts +371 -0
- package/src/services/filter-runtime.ts +821 -0
- package/src/services/filter-settings.ts +30 -0
- package/src/services/identity-resolver.ts +563 -0
- package/src/services/jetstream-sync.ts +636 -0
- package/src/services/lineage-store.ts +89 -0
- package/src/services/link-validator.ts +244 -0
- package/src/services/output-manager.ts +274 -0
- package/src/services/post-parser.ts +62 -0
- package/src/services/profile-resolver.ts +223 -0
- package/src/services/resource-monitor.ts +106 -0
- package/src/services/shared.ts +69 -0
- package/src/services/store-cleaner.ts +43 -0
- package/src/services/store-commit.ts +168 -0
- package/src/services/store-db.ts +248 -0
- package/src/services/store-event-log.ts +285 -0
- package/src/services/store-index-sql.ts +289 -0
- package/src/services/store-index.ts +1152 -0
- package/src/services/store-keys.ts +4 -0
- package/src/services/store-manager.ts +358 -0
- package/src/services/store-stats.ts +522 -0
- package/src/services/store-writer.ts +200 -0
- package/src/services/sync-checkpoint-store.ts +169 -0
- package/src/services/sync-engine.ts +547 -0
- package/src/services/sync-reporter.ts +16 -0
- package/src/services/sync-settings.ts +72 -0
- package/src/services/trending-topics.ts +226 -0
- package/src/services/view-checkpoint-store.ts +238 -0
- package/src/typeclass/chunk.ts +84 -0
|
@@ -0,0 +1,2113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bluesky API client service providing authenticated access to AT Protocol APIs.
|
|
3
|
+
*
|
|
4
|
+
* This service wraps the @atproto/api client with Effect-based error handling,
|
|
5
|
+
* automatic retry logic, and rate limiting. It provides a streaming interface
|
|
6
|
+
* for paginated endpoints and supports multiple authentication sources.
|
|
7
|
+
*
|
|
8
|
+
* ## Authentication
|
|
9
|
+
*
|
|
10
|
+
* Credentials are resolved from (in order of priority):
|
|
11
|
+
* 1. Environment variables (BSKY_HANDLE, BSKY_PASSWORD)
|
|
12
|
+
* 2. Credential store (managed via `skygent credentials` commands)
|
|
13
|
+
* 3. Interactive prompts (if TTY available)
|
|
14
|
+
*
|
|
15
|
+
* ## Features
|
|
16
|
+
*
|
|
17
|
+
* - Automatic session refresh and retry on rate limits
|
|
18
|
+
* - Configurable retry policies via AppConfig
|
|
19
|
+
* - Streaming interface for paginated feeds
|
|
20
|
+
* - Type-safe API wrappers
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { Effect } from "effect";
|
|
25
|
+
* import { BskyClient } from "./services/bsky-client.js";
|
|
26
|
+
*
|
|
27
|
+
* const program = Effect.gen(function* () {
|
|
28
|
+
* const client = yield* BskyClient;
|
|
29
|
+
*
|
|
30
|
+
* // Stream posts from the authenticated user's timeline
|
|
31
|
+
* const posts = yield* client.getTimeline({ limit: 100 }).pipe(
|
|
32
|
+
* Effect.runCollect
|
|
33
|
+
* );
|
|
34
|
+
*
|
|
35
|
+
* // Get a specific post
|
|
36
|
+
* const post = yield* client.getPost("at://did:plc:abc/app.bsky.feed.post/123");
|
|
37
|
+
*
|
|
38
|
+
* // Search for posts
|
|
39
|
+
* const searchResults = yield* client.searchPosts("skygent").pipe(
|
|
40
|
+
* Effect.runCollect
|
|
41
|
+
* );
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @module services/bsky-client
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { AtpAgent, AppBskyFeedDefs } from "@atproto/api";
|
|
49
|
+
import type {
|
|
50
|
+
AppBskyActorSearchActors,
|
|
51
|
+
AppBskyActorSearchActorsTypeahead,
|
|
52
|
+
AppBskyActorGetProfiles,
|
|
53
|
+
AppBskyFeedGetAuthorFeed,
|
|
54
|
+
AppBskyFeedGetFeed,
|
|
55
|
+
AppBskyFeedGetListFeed,
|
|
56
|
+
AppBskyFeedGetPostThread,
|
|
57
|
+
AppBskyFeedGetPosts,
|
|
58
|
+
AppBskyFeedGetFeedGenerator,
|
|
59
|
+
AppBskyFeedGetFeedGenerators,
|
|
60
|
+
AppBskyFeedGetActorFeeds,
|
|
61
|
+
AppBskyFeedGetLikes,
|
|
62
|
+
AppBskyFeedGetQuotes,
|
|
63
|
+
AppBskyFeedGetRepostedBy,
|
|
64
|
+
AppBskyFeedSearchPosts,
|
|
65
|
+
AppBskyFeedGetTimeline,
|
|
66
|
+
AppBskyFeedPost,
|
|
67
|
+
AppBskyGraphGetBlocks,
|
|
68
|
+
AppBskyGraphGetFollowers,
|
|
69
|
+
AppBskyGraphGetFollows,
|
|
70
|
+
AppBskyGraphGetKnownFollowers,
|
|
71
|
+
AppBskyGraphGetList,
|
|
72
|
+
AppBskyGraphGetLists,
|
|
73
|
+
AppBskyGraphGetMutes,
|
|
74
|
+
AppBskyGraphGetRelationships,
|
|
75
|
+
AppBskyNotificationListNotifications,
|
|
76
|
+
AppBskyUnspeccedGetPopularFeedGenerators,
|
|
77
|
+
AppBskyUnspeccedGetTrendingTopics,
|
|
78
|
+
ComAtprotoIdentityResolveIdentity
|
|
79
|
+
} from "@atproto/api";
|
|
80
|
+
import {
|
|
81
|
+
Chunk,
|
|
82
|
+
Clock,
|
|
83
|
+
Config,
|
|
84
|
+
Context,
|
|
85
|
+
Duration,
|
|
86
|
+
Effect,
|
|
87
|
+
Layer,
|
|
88
|
+
Option,
|
|
89
|
+
Redacted,
|
|
90
|
+
Ref,
|
|
91
|
+
Schedule,
|
|
92
|
+
Schema,
|
|
93
|
+
Stream
|
|
94
|
+
} from "effect";
|
|
95
|
+
import { AppConfigService } from "./app-config.js";
|
|
96
|
+
import { CredentialStore } from "./credential-store.js";
|
|
97
|
+
import { BskyError } from "../domain/errors.js";
|
|
98
|
+
import { RawPost } from "../domain/raw.js";
|
|
99
|
+
import {
|
|
100
|
+
BlockedAuthor,
|
|
101
|
+
EmbedAspectRatio,
|
|
102
|
+
EmbedExternal,
|
|
103
|
+
EmbedImage,
|
|
104
|
+
EmbedImages,
|
|
105
|
+
EmbedRecordBlocked,
|
|
106
|
+
EmbedRecordDetached,
|
|
107
|
+
EmbedRecordNotFound,
|
|
108
|
+
EmbedRecordTarget,
|
|
109
|
+
EmbedRecordUnknown,
|
|
110
|
+
EmbedRecordView,
|
|
111
|
+
EmbedRecord,
|
|
112
|
+
EmbedRecordWithMedia,
|
|
113
|
+
EmbedUnknown,
|
|
114
|
+
EmbedVideo,
|
|
115
|
+
FeedContext,
|
|
116
|
+
FeedGeneratorView,
|
|
117
|
+
FeedPostBlocked,
|
|
118
|
+
FeedPostNotFound,
|
|
119
|
+
FeedPostUnknown,
|
|
120
|
+
FeedPostViewRef,
|
|
121
|
+
FeedReasonPin,
|
|
122
|
+
FeedReasonRepost,
|
|
123
|
+
FeedReasonUnknown,
|
|
124
|
+
FeedReplyRef,
|
|
125
|
+
IdentityInfo,
|
|
126
|
+
Label,
|
|
127
|
+
ListItemView,
|
|
128
|
+
ListView,
|
|
129
|
+
PostLike,
|
|
130
|
+
AuthorFeedFilter,
|
|
131
|
+
PostEmbed,
|
|
132
|
+
PostMetrics,
|
|
133
|
+
PostViewerState,
|
|
134
|
+
ProfileBasic,
|
|
135
|
+
ProfileView,
|
|
136
|
+
RelationshipView
|
|
137
|
+
} from "../domain/bsky.js";
|
|
138
|
+
import { Did, PostCid, PostUri, Timestamp } from "../domain/primitives.js";
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Options for retrieving the user's timeline.
|
|
142
|
+
*/
|
|
143
|
+
export interface TimelineOptions {
|
|
144
|
+
/** Maximum number of posts to retrieve per page (default: 100) */
|
|
145
|
+
readonly limit?: number;
|
|
146
|
+
/** Pagination cursor for fetching the next page */
|
|
147
|
+
readonly cursor?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Options for retrieving feed or list posts.
|
|
152
|
+
*/
|
|
153
|
+
export interface FeedOptions {
|
|
154
|
+
/** Maximum number of posts to retrieve per page (default: 100) */
|
|
155
|
+
readonly limit?: number;
|
|
156
|
+
/** Pagination cursor for fetching the next page */
|
|
157
|
+
readonly cursor?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Options for graph-related queries (followers, follows, etc.).
|
|
162
|
+
*/
|
|
163
|
+
export interface GraphOptions {
|
|
164
|
+
/** Maximum number of results per page */
|
|
165
|
+
readonly limit?: number;
|
|
166
|
+
/** Pagination cursor for fetching the next page */
|
|
167
|
+
readonly cursor?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Options for retrieving Bluesky lists.
|
|
172
|
+
*/
|
|
173
|
+
export interface GraphListsOptions {
|
|
174
|
+
/** Maximum number of lists per page */
|
|
175
|
+
readonly limit?: number;
|
|
176
|
+
/** Pagination cursor for fetching the next page */
|
|
177
|
+
readonly cursor?: string;
|
|
178
|
+
/** Filter by list purpose ("modlist" or "curatelist") */
|
|
179
|
+
readonly purposes?: ReadonlyArray<"modlist" | "curatelist">;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Options for retrieving an author's feed.
|
|
184
|
+
*/
|
|
185
|
+
export interface AuthorFeedOptions {
|
|
186
|
+
/** Maximum number of posts per page */
|
|
187
|
+
readonly limit?: number;
|
|
188
|
+
/** Pagination cursor for fetching the next page */
|
|
189
|
+
readonly cursor?: string;
|
|
190
|
+
/** Filter posts by type (posts, posts_and_author_threads, posts_no_replies, posts_with_media, posts_with_video) */
|
|
191
|
+
readonly filter?: AuthorFeedFilter;
|
|
192
|
+
/** Whether to include pinned posts */
|
|
193
|
+
readonly includePins?: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Options for retrieving a post thread.
|
|
198
|
+
*/
|
|
199
|
+
export interface ThreadOptions {
|
|
200
|
+
/** How many levels of replies to fetch (default: 6) */
|
|
201
|
+
readonly depth?: number;
|
|
202
|
+
/** How many levels of parent posts to fetch (default: 6) */
|
|
203
|
+
readonly parentHeight?: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Options for retrieving notifications.
|
|
208
|
+
*/
|
|
209
|
+
export interface NotificationsOptions {
|
|
210
|
+
/** Maximum number of notifications per page */
|
|
211
|
+
readonly limit?: number;
|
|
212
|
+
/** Pagination cursor for fetching the next page */
|
|
213
|
+
readonly cursor?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Options for searching actors (users).
|
|
218
|
+
*/
|
|
219
|
+
export interface ActorSearchOptions {
|
|
220
|
+
/** Maximum number of results per page */
|
|
221
|
+
readonly limit?: number;
|
|
222
|
+
/** Pagination cursor for fetching the next page */
|
|
223
|
+
readonly cursor?: string;
|
|
224
|
+
/** Use typeahead search for faster results */
|
|
225
|
+
readonly typeahead?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Options for searching feeds.
|
|
230
|
+
*/
|
|
231
|
+
export interface FeedSearchOptions {
|
|
232
|
+
/** Maximum number of results per page */
|
|
233
|
+
readonly limit?: number;
|
|
234
|
+
/** Pagination cursor for fetching the next page */
|
|
235
|
+
readonly cursor?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Options for retrieving an actor's feeds.
|
|
240
|
+
*/
|
|
241
|
+
export interface ActorFeedsOptions {
|
|
242
|
+
/** Maximum number of feeds per page */
|
|
243
|
+
readonly limit?: number;
|
|
244
|
+
/** Pagination cursor for fetching the next page */
|
|
245
|
+
readonly cursor?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Options for retrieving engagement data (likes, reposts, quotes).
|
|
250
|
+
*/
|
|
251
|
+
export interface EngagementOptions {
|
|
252
|
+
/** The post CID to query engagement for */
|
|
253
|
+
readonly cid?: string;
|
|
254
|
+
/** Maximum number of results per page */
|
|
255
|
+
readonly limit?: number;
|
|
256
|
+
/** Pagination cursor for fetching the next page */
|
|
257
|
+
readonly cursor?: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Options for searching posts across the Bluesky network.
|
|
262
|
+
*/
|
|
263
|
+
export interface NetworkSearchOptions {
|
|
264
|
+
/** Maximum number of results per page */
|
|
265
|
+
readonly limit?: number;
|
|
266
|
+
/** Pagination cursor for fetching the next page */
|
|
267
|
+
readonly cursor?: string;
|
|
268
|
+
/** Sort order: "top" for relevance, "latest" for recency */
|
|
269
|
+
readonly sort?: "top" | "latest";
|
|
270
|
+
/** Filter posts after this timestamp (ISO 8601) */
|
|
271
|
+
readonly since?: string;
|
|
272
|
+
/** Filter posts before this timestamp (ISO 8601) */
|
|
273
|
+
readonly until?: string;
|
|
274
|
+
/** Filter posts mentioning this handle */
|
|
275
|
+
readonly mentions?: string;
|
|
276
|
+
/** Filter posts by this author handle */
|
|
277
|
+
readonly author?: string;
|
|
278
|
+
/** Filter posts by language code */
|
|
279
|
+
readonly lang?: string;
|
|
280
|
+
/** Filter posts containing links to this domain */
|
|
281
|
+
readonly domain?: string;
|
|
282
|
+
/** Filter posts containing this URL */
|
|
283
|
+
readonly url?: string;
|
|
284
|
+
/** Filter posts containing all of these hashtags */
|
|
285
|
+
readonly tags?: ReadonlyArray<string>;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
type FeedViewPost = AppBskyFeedDefs.FeedViewPost;
|
|
289
|
+
type PostView = AppBskyFeedDefs.PostView;
|
|
290
|
+
type ThreadViewPost = AppBskyFeedDefs.ThreadViewPost;
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
const extractBskyErrorDetails = (cause: unknown) => {
|
|
294
|
+
if (!cause || typeof cause !== "object") {
|
|
295
|
+
return {} as const;
|
|
296
|
+
}
|
|
297
|
+
const record = cause as {
|
|
298
|
+
status?: unknown;
|
|
299
|
+
statusCode?: unknown;
|
|
300
|
+
error?: unknown;
|
|
301
|
+
message?: unknown;
|
|
302
|
+
};
|
|
303
|
+
const status =
|
|
304
|
+
typeof record.status === "number"
|
|
305
|
+
? record.status
|
|
306
|
+
: typeof record.statusCode === "number"
|
|
307
|
+
? record.statusCode
|
|
308
|
+
: typeof (record.error as { status?: unknown })?.status === "number"
|
|
309
|
+
? (record.error as { status: number }).status
|
|
310
|
+
: undefined;
|
|
311
|
+
const error =
|
|
312
|
+
typeof record.error === "string"
|
|
313
|
+
? record.error
|
|
314
|
+
: typeof (record.error as { message?: unknown })?.message === "string"
|
|
315
|
+
? (record.error as { message: string }).message
|
|
316
|
+
: undefined;
|
|
317
|
+
const detail =
|
|
318
|
+
typeof record.message === "string"
|
|
319
|
+
? record.message
|
|
320
|
+
: typeof (record.error as { message?: unknown })?.message === "string"
|
|
321
|
+
? (record.error as { message: string }).message
|
|
322
|
+
: undefined;
|
|
323
|
+
return {
|
|
324
|
+
...(status !== undefined ? { status } : {}),
|
|
325
|
+
...(error !== undefined ? { error } : {}),
|
|
326
|
+
...(detail !== undefined ? { detail } : {})
|
|
327
|
+
} as const;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const formatBskyErrorMessage = (
|
|
331
|
+
fallback: string,
|
|
332
|
+
_cause: unknown,
|
|
333
|
+
details: ReturnType<typeof extractBskyErrorDetails>
|
|
334
|
+
) => {
|
|
335
|
+
const status = details.status;
|
|
336
|
+
const summary = typeof status === "number" ? statusSummary(status) : undefined;
|
|
337
|
+
const detail = normalizeDetail(details.detail ?? details.error);
|
|
338
|
+
|
|
339
|
+
if (typeof status === "number") {
|
|
340
|
+
const summaryPart = summary ? `: ${summary}` : "";
|
|
341
|
+
const detailPart = detail && detail !== summary ? ` - ${detail}` : "";
|
|
342
|
+
return `${fallback} (HTTP ${status}${summaryPart}${detailPart})`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return detail ? `${fallback} (${detail})` : fallback;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const statusSummary = (status: number): string | undefined => {
|
|
349
|
+
switch (status) {
|
|
350
|
+
case 400:
|
|
351
|
+
return "Bad request";
|
|
352
|
+
case 401:
|
|
353
|
+
return "Unauthorized";
|
|
354
|
+
case 403:
|
|
355
|
+
return "Forbidden";
|
|
356
|
+
case 404:
|
|
357
|
+
return "Not found";
|
|
358
|
+
case 409:
|
|
359
|
+
return "Conflict";
|
|
360
|
+
case 413:
|
|
361
|
+
return "Payload too large";
|
|
362
|
+
case 429:
|
|
363
|
+
return "Rate limited";
|
|
364
|
+
case 500:
|
|
365
|
+
return "Server error";
|
|
366
|
+
case 502:
|
|
367
|
+
return "Bad gateway";
|
|
368
|
+
case 503:
|
|
369
|
+
return "Service unavailable";
|
|
370
|
+
case 504:
|
|
371
|
+
return "Gateway timeout";
|
|
372
|
+
default:
|
|
373
|
+
return status >= 500 ? "Server error" : undefined;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const normalizeDetail = (detail?: string) => {
|
|
378
|
+
if (!detail) return undefined;
|
|
379
|
+
const trimmed = detail.trim();
|
|
380
|
+
if (trimmed.length === 0) return undefined;
|
|
381
|
+
return trimmed.length > 160 ? `${trimmed.slice(0, 157)}...` : trimmed;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const toBskyError = (message: string, operation?: string) => (cause: unknown) => {
|
|
385
|
+
const details = extractBskyErrorDetails(cause);
|
|
386
|
+
return BskyError.make({
|
|
387
|
+
message: formatBskyErrorMessage(message, cause, details),
|
|
388
|
+
cause,
|
|
389
|
+
operation,
|
|
390
|
+
...details
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const isRetryableCause = (cause: unknown) => {
|
|
395
|
+
if (!cause || typeof cause !== "object") return false;
|
|
396
|
+
const source =
|
|
397
|
+
"cause" in cause && typeof (cause as { cause?: unknown }).cause !== "undefined"
|
|
398
|
+
? (cause as { cause?: unknown }).cause
|
|
399
|
+
: cause;
|
|
400
|
+
if (!source || typeof source !== "object") return false;
|
|
401
|
+
const record = source as { status?: unknown; statusCode?: unknown; error?: unknown };
|
|
402
|
+
const status =
|
|
403
|
+
typeof record.status === "number"
|
|
404
|
+
? record.status
|
|
405
|
+
: typeof record.statusCode === "number"
|
|
406
|
+
? record.statusCode
|
|
407
|
+
: typeof (record.error as { status?: unknown })?.status === "number"
|
|
408
|
+
? (record.error as { status: number }).status
|
|
409
|
+
: undefined;
|
|
410
|
+
if (typeof status === "number") {
|
|
411
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
412
|
+
}
|
|
413
|
+
const code = (record as { code?: unknown }).code;
|
|
414
|
+
return typeof code === "string" && ["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN"].includes(code);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const mapAspectRatio = (input: unknown) => {
|
|
418
|
+
if (!input || typeof input !== "object") return undefined;
|
|
419
|
+
const ratio = input as { width?: unknown; height?: unknown };
|
|
420
|
+
if (typeof ratio.width !== "number" || typeof ratio.height !== "number") {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
return EmbedAspectRatio.make({ width: ratio.width, height: ratio.height });
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const decodeTimestamp = (value: unknown, message: string) =>
|
|
427
|
+
Schema.decodeUnknown(Timestamp)(value).pipe(
|
|
428
|
+
Effect.mapError(toBskyError(message))
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const decodeDid = (value: unknown, message: string) =>
|
|
432
|
+
Schema.decodeUnknown(Did)(value).pipe(
|
|
433
|
+
Effect.mapError(toBskyError(message))
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const decodePostUri = (value: unknown, message: string) =>
|
|
437
|
+
Schema.decodeUnknown(PostUri)(value).pipe(
|
|
438
|
+
Effect.mapError(toBskyError(message))
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const decodePostCid = (value: unknown, message: string) =>
|
|
442
|
+
Schema.decodeUnknown(PostCid)(value).pipe(
|
|
443
|
+
Effect.mapError(toBskyError(message))
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const decodePostUriOptional = (value: unknown, message: string) =>
|
|
447
|
+
typeof value === "undefined"
|
|
448
|
+
? Effect.void.pipe(Effect.as(undefined))
|
|
449
|
+
: decodePostUri(value, message);
|
|
450
|
+
|
|
451
|
+
const decodePostCidOptional = (value: unknown, message: string) =>
|
|
452
|
+
typeof value === "undefined"
|
|
453
|
+
? Effect.void.pipe(Effect.as(undefined))
|
|
454
|
+
: decodePostCid(value, message);
|
|
455
|
+
|
|
456
|
+
const decodeLabels = (labels: unknown) =>
|
|
457
|
+
Schema.decodeUnknown(Schema.Array(Label))(labels).pipe(
|
|
458
|
+
Effect.mapError(toBskyError("Invalid moderation labels"))
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const decodeProfileBasic = (input: unknown) =>
|
|
462
|
+
Effect.gen(function* () {
|
|
463
|
+
if (!input || typeof input !== "object") {
|
|
464
|
+
return yield* BskyError.make({ message: "Invalid author payload" });
|
|
465
|
+
}
|
|
466
|
+
const author = input as Record<string, unknown>;
|
|
467
|
+
return yield* Schema.decodeUnknown(ProfileBasic)({
|
|
468
|
+
did: author.did,
|
|
469
|
+
handle: author.handle,
|
|
470
|
+
displayName: author.displayName,
|
|
471
|
+
pronouns: author.pronouns,
|
|
472
|
+
avatar: author.avatar,
|
|
473
|
+
associated: author.associated,
|
|
474
|
+
viewer: author.viewer,
|
|
475
|
+
labels: author.labels,
|
|
476
|
+
createdAt: author.createdAt,
|
|
477
|
+
verification: author.verification,
|
|
478
|
+
status: author.status,
|
|
479
|
+
debug: author.debug
|
|
480
|
+
}).pipe(Effect.mapError(toBskyError("Invalid author payload")));
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const decodeProfileView = (input: unknown) =>
|
|
484
|
+
Effect.gen(function* () {
|
|
485
|
+
if (!input || typeof input !== "object") {
|
|
486
|
+
return yield* BskyError.make({ message: "Invalid profile payload" });
|
|
487
|
+
}
|
|
488
|
+
const author = input as Record<string, unknown>;
|
|
489
|
+
return yield* Schema.decodeUnknown(ProfileView)({
|
|
490
|
+
did: author.did,
|
|
491
|
+
handle: author.handle,
|
|
492
|
+
displayName: author.displayName,
|
|
493
|
+
pronouns: author.pronouns,
|
|
494
|
+
description: author.description,
|
|
495
|
+
avatar: author.avatar,
|
|
496
|
+
associated: author.associated,
|
|
497
|
+
indexedAt: author.indexedAt,
|
|
498
|
+
createdAt: author.createdAt,
|
|
499
|
+
viewer: author.viewer,
|
|
500
|
+
labels: author.labels,
|
|
501
|
+
verification: author.verification,
|
|
502
|
+
status: author.status,
|
|
503
|
+
debug: author.debug
|
|
504
|
+
}).pipe(Effect.mapError(toBskyError("Invalid profile payload")));
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const decodeIdentityInfo = (input: unknown) =>
|
|
508
|
+
Effect.gen(function* () {
|
|
509
|
+
if (!input || typeof input !== "object") {
|
|
510
|
+
return yield* BskyError.make({ message: "Invalid identity payload" });
|
|
511
|
+
}
|
|
512
|
+
const info = input as Record<string, unknown>;
|
|
513
|
+
return yield* Schema.decodeUnknown(IdentityInfo)({
|
|
514
|
+
did: info.did,
|
|
515
|
+
handle: info.handle,
|
|
516
|
+
didDoc: info.didDoc
|
|
517
|
+
}).pipe(Effect.mapError(toBskyError("Invalid identity payload")));
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const decodeFeedGeneratorView = (input: unknown) =>
|
|
521
|
+
Effect.gen(function* () {
|
|
522
|
+
if (!input || typeof input !== "object") {
|
|
523
|
+
return yield* BskyError.make({ message: "Invalid feed generator payload" });
|
|
524
|
+
}
|
|
525
|
+
const feed = input as Record<string, unknown>;
|
|
526
|
+
const creator = yield* decodeProfileView(feed.creator);
|
|
527
|
+
return yield* Schema.decodeUnknown(FeedGeneratorView)({
|
|
528
|
+
uri: feed.uri,
|
|
529
|
+
cid: feed.cid,
|
|
530
|
+
did: feed.did,
|
|
531
|
+
creator,
|
|
532
|
+
displayName: feed.displayName,
|
|
533
|
+
description: feed.description,
|
|
534
|
+
descriptionFacets: feed.descriptionFacets,
|
|
535
|
+
avatar: feed.avatar,
|
|
536
|
+
likeCount: feed.likeCount,
|
|
537
|
+
acceptsInteractions: feed.acceptsInteractions,
|
|
538
|
+
labels: feed.labels,
|
|
539
|
+
viewer: feed.viewer,
|
|
540
|
+
contentMode: feed.contentMode,
|
|
541
|
+
indexedAt: feed.indexedAt
|
|
542
|
+
}).pipe(Effect.mapError(toBskyError("Invalid feed generator payload")));
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const decodeListView = (input: unknown) =>
|
|
546
|
+
Effect.gen(function* () {
|
|
547
|
+
if (!input || typeof input !== "object") {
|
|
548
|
+
return yield* BskyError.make({ message: "Invalid list payload" });
|
|
549
|
+
}
|
|
550
|
+
const list = input as Record<string, unknown>;
|
|
551
|
+
const creator = yield* decodeProfileView(list.creator);
|
|
552
|
+
return yield* Schema.decodeUnknown(ListView)({
|
|
553
|
+
uri: list.uri,
|
|
554
|
+
cid: list.cid,
|
|
555
|
+
creator,
|
|
556
|
+
name: list.name,
|
|
557
|
+
purpose: list.purpose,
|
|
558
|
+
description: list.description,
|
|
559
|
+
descriptionFacets: list.descriptionFacets,
|
|
560
|
+
avatar: list.avatar,
|
|
561
|
+
listItemCount: list.listItemCount,
|
|
562
|
+
labels: list.labels,
|
|
563
|
+
viewer: list.viewer,
|
|
564
|
+
indexedAt: list.indexedAt
|
|
565
|
+
}).pipe(Effect.mapError(toBskyError("Invalid list payload")));
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const decodeListItemView = (input: unknown) =>
|
|
569
|
+
Effect.gen(function* () {
|
|
570
|
+
if (!input || typeof input !== "object") {
|
|
571
|
+
return yield* BskyError.make({ message: "Invalid list item payload" });
|
|
572
|
+
}
|
|
573
|
+
const item = input as Record<string, unknown>;
|
|
574
|
+
const subject = yield* decodeProfileView(item.subject);
|
|
575
|
+
return yield* Schema.decodeUnknown(ListItemView)({
|
|
576
|
+
uri: item.uri,
|
|
577
|
+
subject
|
|
578
|
+
}).pipe(Effect.mapError(toBskyError("Invalid list item payload")));
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const decodeRelationshipView = (input: unknown) =>
|
|
582
|
+
Schema.decodeUnknown(RelationshipView)(input).pipe(
|
|
583
|
+
Effect.mapError(toBskyError("Invalid relationship payload"))
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
const decodePostLike = (input: unknown) =>
|
|
587
|
+
Effect.gen(function* () {
|
|
588
|
+
if (!input || typeof input !== "object") {
|
|
589
|
+
return yield* BskyError.make({ message: "Invalid like payload" });
|
|
590
|
+
}
|
|
591
|
+
const like = input as Record<string, unknown>;
|
|
592
|
+
const actor = yield* decodeProfileView(like.actor);
|
|
593
|
+
const createdAt = yield* decodeTimestamp(like.createdAt, "Invalid like timestamp");
|
|
594
|
+
const indexedAt = yield* decodeTimestamp(like.indexedAt, "Invalid like timestamp");
|
|
595
|
+
return yield* Schema.decodeUnknown(PostLike)({
|
|
596
|
+
actor,
|
|
597
|
+
createdAt,
|
|
598
|
+
indexedAt
|
|
599
|
+
}).pipe(Effect.mapError(toBskyError("Invalid like payload")));
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const decodeViewerState = (input: unknown) =>
|
|
603
|
+
Schema.decodeUnknown(PostViewerState)(input).pipe(
|
|
604
|
+
Effect.mapError(toBskyError("Invalid viewer state"))
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const mapBlockedAuthor = (input: unknown) =>
|
|
608
|
+
Effect.gen(function* () {
|
|
609
|
+
if (!input || typeof input !== "object") {
|
|
610
|
+
return yield* BskyError.make({ message: "Invalid blocked author payload" });
|
|
611
|
+
}
|
|
612
|
+
const author = input as Record<string, unknown>;
|
|
613
|
+
return yield* Schema.decodeUnknown(BlockedAuthor)({
|
|
614
|
+
did: author.did,
|
|
615
|
+
viewer: author.viewer
|
|
616
|
+
}).pipe(Effect.mapError(toBskyError("Invalid blocked author payload")));
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const mapEmbedRecordTarget = (
|
|
620
|
+
record: unknown
|
|
621
|
+
): Effect.Effect<EmbedRecordTarget, BskyError> =>
|
|
622
|
+
Effect.gen(function* () {
|
|
623
|
+
if (!record || typeof record !== "object") {
|
|
624
|
+
return EmbedRecordUnknown.make({ rawType: "unknown", data: record });
|
|
625
|
+
}
|
|
626
|
+
const typed = record as { $type?: string };
|
|
627
|
+
const recordType = typed.$type;
|
|
628
|
+
switch (recordType) {
|
|
629
|
+
case "app.bsky.embed.record#viewRecord": {
|
|
630
|
+
const view = record as {
|
|
631
|
+
uri?: unknown;
|
|
632
|
+
cid?: unknown;
|
|
633
|
+
author?: unknown;
|
|
634
|
+
value?: unknown;
|
|
635
|
+
labels?: unknown;
|
|
636
|
+
replyCount?: unknown;
|
|
637
|
+
repostCount?: unknown;
|
|
638
|
+
likeCount?: unknown;
|
|
639
|
+
quoteCount?: unknown;
|
|
640
|
+
embeds?: unknown;
|
|
641
|
+
indexedAt?: unknown;
|
|
642
|
+
};
|
|
643
|
+
const author = yield* decodeProfileBasic(view.author);
|
|
644
|
+
const labels =
|
|
645
|
+
view.labels && Array.isArray(view.labels)
|
|
646
|
+
? yield* decodeLabels(view.labels)
|
|
647
|
+
: undefined;
|
|
648
|
+
const metrics = (() => {
|
|
649
|
+
const data = {
|
|
650
|
+
replyCount: view.replyCount as number | undefined,
|
|
651
|
+
repostCount: view.repostCount as number | undefined,
|
|
652
|
+
likeCount: view.likeCount as number | undefined,
|
|
653
|
+
quoteCount: view.quoteCount as number | undefined
|
|
654
|
+
};
|
|
655
|
+
const hasAny = Object.values(data).some((value) => value !== undefined);
|
|
656
|
+
return hasAny ? PostMetrics.make(data) : undefined;
|
|
657
|
+
})();
|
|
658
|
+
const indexedAt = yield* decodeTimestamp(
|
|
659
|
+
view.indexedAt,
|
|
660
|
+
"Invalid record embed timestamp"
|
|
661
|
+
);
|
|
662
|
+
const embeds: Array<PostEmbed> | undefined =
|
|
663
|
+
Array.isArray(view.embeds)
|
|
664
|
+
? yield* Effect.forEach(
|
|
665
|
+
view.embeds,
|
|
666
|
+
(entry) => mapEmbedView(entry),
|
|
667
|
+
{ concurrency: "unbounded" }
|
|
668
|
+
).pipe(
|
|
669
|
+
Effect.map((values) =>
|
|
670
|
+
values.filter((value): value is PostEmbed => value !== undefined)
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
: undefined;
|
|
674
|
+
return EmbedRecordView.make({
|
|
675
|
+
uri: yield* decodePostUri(
|
|
676
|
+
view.uri,
|
|
677
|
+
"Invalid record embed URI"
|
|
678
|
+
),
|
|
679
|
+
cid: yield* decodePostCid(
|
|
680
|
+
view.cid,
|
|
681
|
+
"Invalid record embed CID"
|
|
682
|
+
),
|
|
683
|
+
author,
|
|
684
|
+
value: view.value ?? record,
|
|
685
|
+
labels,
|
|
686
|
+
metrics,
|
|
687
|
+
embeds,
|
|
688
|
+
indexedAt
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
case "app.bsky.embed.record#viewNotFound":
|
|
692
|
+
return EmbedRecordNotFound.make({
|
|
693
|
+
uri: yield* decodePostUri(
|
|
694
|
+
(record as { uri?: unknown }).uri,
|
|
695
|
+
"Invalid record embed URI"
|
|
696
|
+
),
|
|
697
|
+
notFound: true
|
|
698
|
+
});
|
|
699
|
+
case "app.bsky.embed.record#viewBlocked": {
|
|
700
|
+
const author = yield* mapBlockedAuthor(
|
|
701
|
+
(record as { author?: unknown }).author
|
|
702
|
+
);
|
|
703
|
+
return EmbedRecordBlocked.make({
|
|
704
|
+
uri: yield* decodePostUri(
|
|
705
|
+
(record as { uri?: unknown }).uri,
|
|
706
|
+
"Invalid record embed URI"
|
|
707
|
+
),
|
|
708
|
+
blocked: true,
|
|
709
|
+
author
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
case "app.bsky.embed.record#viewDetached":
|
|
713
|
+
return EmbedRecordDetached.make({
|
|
714
|
+
uri: yield* decodePostUri(
|
|
715
|
+
(record as { uri?: unknown }).uri,
|
|
716
|
+
"Invalid record embed URI"
|
|
717
|
+
),
|
|
718
|
+
detached: true
|
|
719
|
+
});
|
|
720
|
+
default:
|
|
721
|
+
return EmbedRecordUnknown.make({
|
|
722
|
+
rawType: typeof recordType === "string" ? recordType : "unknown",
|
|
723
|
+
data: record
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const mapEmbedView = (
|
|
729
|
+
embed: unknown
|
|
730
|
+
): Effect.Effect<PostEmbed | undefined, BskyError> =>
|
|
731
|
+
Effect.gen(function* () {
|
|
732
|
+
if (!embed || typeof embed !== "object") return undefined;
|
|
733
|
+
const typed = embed as { $type?: string };
|
|
734
|
+
switch (typed.$type) {
|
|
735
|
+
case "app.bsky.embed.images#view": {
|
|
736
|
+
const images = (embed as { images?: Array<any> }).images ?? [];
|
|
737
|
+
return EmbedImages.make({
|
|
738
|
+
images: images
|
|
739
|
+
.filter((image) => image && typeof image === "object")
|
|
740
|
+
.map((image) =>
|
|
741
|
+
EmbedImage.make({
|
|
742
|
+
thumb: String(image.thumb ?? ""),
|
|
743
|
+
fullsize: String(image.fullsize ?? ""),
|
|
744
|
+
alt: String(image.alt ?? ""),
|
|
745
|
+
aspectRatio: mapAspectRatio(image.aspectRatio)
|
|
746
|
+
})
|
|
747
|
+
)
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
case "app.bsky.embed.external#view": {
|
|
751
|
+
const external = (embed as { external?: any }).external ?? {};
|
|
752
|
+
return EmbedExternal.make({
|
|
753
|
+
uri: String(external.uri ?? ""),
|
|
754
|
+
title: String(external.title ?? ""),
|
|
755
|
+
description: String(external.description ?? ""),
|
|
756
|
+
thumb: external.thumb ? String(external.thumb) : undefined
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
case "app.bsky.embed.video#view": {
|
|
760
|
+
return EmbedVideo.make({
|
|
761
|
+
cid: String((embed as { cid?: unknown }).cid ?? ""),
|
|
762
|
+
playlist: String((embed as { playlist?: unknown }).playlist ?? ""),
|
|
763
|
+
thumbnail: (embed as { thumbnail?: unknown }).thumbnail
|
|
764
|
+
? String((embed as { thumbnail?: unknown }).thumbnail)
|
|
765
|
+
: undefined,
|
|
766
|
+
alt: (embed as { alt?: unknown }).alt
|
|
767
|
+
? String((embed as { alt?: unknown }).alt)
|
|
768
|
+
: undefined,
|
|
769
|
+
aspectRatio: mapAspectRatio((embed as { aspectRatio?: unknown }).aspectRatio)
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
case "app.bsky.embed.record#view": {
|
|
773
|
+
const record = (embed as { record?: unknown }).record;
|
|
774
|
+
const recordType = record && typeof record === "object" ? (record as { $type?: string }).$type : undefined;
|
|
775
|
+
const mapped = yield* mapEmbedRecordTarget(record ?? embed);
|
|
776
|
+
return EmbedRecord.make({
|
|
777
|
+
recordType,
|
|
778
|
+
record: mapped
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
case "app.bsky.embed.recordWithMedia#view": {
|
|
782
|
+
const record = (embed as { record?: unknown }).record;
|
|
783
|
+
const recordType = record && typeof record === "object" ? (record as { $type?: string }).$type : undefined;
|
|
784
|
+
const mediaCandidate: PostEmbed | undefined = yield* mapEmbedView(
|
|
785
|
+
(embed as { media?: unknown }).media
|
|
786
|
+
);
|
|
787
|
+
const media: PostEmbed | unknown =
|
|
788
|
+
mediaCandidate &&
|
|
789
|
+
(mediaCandidate._tag === "Images" ||
|
|
790
|
+
mediaCandidate._tag === "External" ||
|
|
791
|
+
mediaCandidate._tag === "Video")
|
|
792
|
+
? mediaCandidate
|
|
793
|
+
: (embed as { media?: unknown }).media;
|
|
794
|
+
const mapped: EmbedRecordTarget = yield* mapEmbedRecordTarget(record ?? embed);
|
|
795
|
+
return EmbedRecordWithMedia.make({
|
|
796
|
+
recordType,
|
|
797
|
+
record: mapped,
|
|
798
|
+
media
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
default:
|
|
802
|
+
if (typed.$type) {
|
|
803
|
+
return EmbedUnknown.make({ rawType: typed.$type, data: embed });
|
|
804
|
+
}
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
const metricsFromPostView = (post: PostView) => {
|
|
810
|
+
const data = {
|
|
811
|
+
replyCount: post.replyCount,
|
|
812
|
+
repostCount: post.repostCount,
|
|
813
|
+
likeCount: post.likeCount,
|
|
814
|
+
quoteCount: post.quoteCount,
|
|
815
|
+
bookmarkCount: post.bookmarkCount
|
|
816
|
+
};
|
|
817
|
+
const hasAny = Object.values(data).some((value) => value !== undefined);
|
|
818
|
+
return hasAny ? PostMetrics.make(data) : undefined;
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const mapFeedPostReference = (input: unknown) =>
|
|
822
|
+
Effect.gen(function* () {
|
|
823
|
+
if (!input || typeof input !== "object") {
|
|
824
|
+
return FeedPostUnknown.make({ rawType: "unknown", data: input });
|
|
825
|
+
}
|
|
826
|
+
const candidate = input as {
|
|
827
|
+
$type?: unknown;
|
|
828
|
+
uri?: unknown;
|
|
829
|
+
cid?: unknown;
|
|
830
|
+
author?: unknown;
|
|
831
|
+
notFound?: unknown;
|
|
832
|
+
blocked?: unknown;
|
|
833
|
+
};
|
|
834
|
+
const type = typeof candidate.$type === "string" ? candidate.$type : undefined;
|
|
835
|
+
if (
|
|
836
|
+
type === "app.bsky.feed.defs#postView" ||
|
|
837
|
+
(candidate.uri && candidate.cid && candidate.author)
|
|
838
|
+
) {
|
|
839
|
+
const post = input as PostView;
|
|
840
|
+
const author = yield* decodeProfileBasic(post.author);
|
|
841
|
+
const labels =
|
|
842
|
+
post.labels && post.labels.length > 0
|
|
843
|
+
? yield* decodeLabels(post.labels)
|
|
844
|
+
: undefined;
|
|
845
|
+
const viewer = post.viewer ? yield* decodeViewerState(post.viewer) : undefined;
|
|
846
|
+
const indexedAt = yield* decodeTimestamp(
|
|
847
|
+
post.indexedAt,
|
|
848
|
+
"Invalid feed post timestamp"
|
|
849
|
+
);
|
|
850
|
+
return FeedPostViewRef.make({
|
|
851
|
+
uri: yield* decodePostUri(
|
|
852
|
+
post.uri,
|
|
853
|
+
"Invalid feed post URI"
|
|
854
|
+
),
|
|
855
|
+
cid: yield* decodePostCid(
|
|
856
|
+
post.cid,
|
|
857
|
+
"Invalid feed post CID"
|
|
858
|
+
),
|
|
859
|
+
author,
|
|
860
|
+
indexedAt,
|
|
861
|
+
labels,
|
|
862
|
+
viewer
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (type === "app.bsky.feed.defs#notFoundPost" || candidate.notFound === true) {
|
|
866
|
+
return FeedPostNotFound.make({
|
|
867
|
+
uri: yield* decodePostUri(
|
|
868
|
+
candidate.uri,
|
|
869
|
+
"Invalid feed post URI"
|
|
870
|
+
),
|
|
871
|
+
notFound: true
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
if (type === "app.bsky.feed.defs#blockedPost" || candidate.blocked === true) {
|
|
875
|
+
const author = yield* mapBlockedAuthor(
|
|
876
|
+
(candidate as { author?: unknown }).author
|
|
877
|
+
);
|
|
878
|
+
return FeedPostBlocked.make({
|
|
879
|
+
uri: yield* decodePostUri(
|
|
880
|
+
candidate.uri,
|
|
881
|
+
"Invalid feed post URI"
|
|
882
|
+
),
|
|
883
|
+
blocked: true,
|
|
884
|
+
author
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
return FeedPostUnknown.make({
|
|
888
|
+
rawType: type ?? "unknown",
|
|
889
|
+
data: input
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
const mapFeedReplyRef = (input: unknown) =>
|
|
894
|
+
Effect.gen(function* () {
|
|
895
|
+
if (!input || typeof input !== "object") {
|
|
896
|
+
return yield* BskyError.make({ message: "Invalid feed reply payload" });
|
|
897
|
+
}
|
|
898
|
+
const reply = input as {
|
|
899
|
+
root?: unknown;
|
|
900
|
+
parent?: unknown;
|
|
901
|
+
grandparentAuthor?: unknown;
|
|
902
|
+
};
|
|
903
|
+
const root = yield* mapFeedPostReference(reply.root);
|
|
904
|
+
const parent = yield* mapFeedPostReference(reply.parent);
|
|
905
|
+
const grandparentAuthor = reply.grandparentAuthor
|
|
906
|
+
? yield* decodeProfileBasic(reply.grandparentAuthor)
|
|
907
|
+
: undefined;
|
|
908
|
+
return FeedReplyRef.make({
|
|
909
|
+
root,
|
|
910
|
+
parent,
|
|
911
|
+
grandparentAuthor
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
const mapFeedReason = (input: unknown) =>
|
|
916
|
+
Effect.gen(function* () {
|
|
917
|
+
if (!input || typeof input !== "object") {
|
|
918
|
+
return FeedReasonUnknown.make({ rawType: "unknown", data: input });
|
|
919
|
+
}
|
|
920
|
+
const reason = input as { $type?: unknown };
|
|
921
|
+
const type = typeof reason.$type === "string" ? reason.$type : undefined;
|
|
922
|
+
switch (type) {
|
|
923
|
+
case "app.bsky.feed.defs#reasonRepost": {
|
|
924
|
+
const raw = reason as {
|
|
925
|
+
by?: unknown;
|
|
926
|
+
uri?: unknown;
|
|
927
|
+
cid?: unknown;
|
|
928
|
+
indexedAt?: unknown;
|
|
929
|
+
};
|
|
930
|
+
const by = yield* decodeProfileBasic(raw.by);
|
|
931
|
+
const indexedAt = yield* decodeTimestamp(
|
|
932
|
+
raw.indexedAt,
|
|
933
|
+
"Invalid reason timestamp"
|
|
934
|
+
);
|
|
935
|
+
return FeedReasonRepost.make({
|
|
936
|
+
by,
|
|
937
|
+
uri: yield* decodePostUriOptional(
|
|
938
|
+
raw.uri,
|
|
939
|
+
"Invalid repost URI"
|
|
940
|
+
),
|
|
941
|
+
cid: yield* decodePostCidOptional(
|
|
942
|
+
raw.cid,
|
|
943
|
+
"Invalid repost CID"
|
|
944
|
+
),
|
|
945
|
+
indexedAt
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
case "app.bsky.feed.defs#reasonPin":
|
|
949
|
+
return FeedReasonPin.make({});
|
|
950
|
+
default:
|
|
951
|
+
return FeedReasonUnknown.make({
|
|
952
|
+
rawType: type ?? "unknown",
|
|
953
|
+
data: input
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
const mapFeedContext = (item: FeedViewPost) =>
|
|
959
|
+
Effect.gen(function* () {
|
|
960
|
+
const reply = item.reply ? yield* mapFeedReplyRef(item.reply) : undefined;
|
|
961
|
+
const reason = item.reason ? yield* mapFeedReason(item.reason) : undefined;
|
|
962
|
+
return yield* Schema.decodeUnknown(FeedContext)({
|
|
963
|
+
reply,
|
|
964
|
+
reason,
|
|
965
|
+
feedContext: item.feedContext,
|
|
966
|
+
reqId: item.reqId
|
|
967
|
+
}).pipe(Effect.mapError(toBskyError("Invalid feed context payload")));
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
const withCursor = <T extends Record<string, unknown>>(
|
|
971
|
+
params: T,
|
|
972
|
+
cursor: string | undefined
|
|
973
|
+
): T & { cursor?: string } =>
|
|
974
|
+
typeof cursor === "string" ? { ...params, cursor } : params;
|
|
975
|
+
|
|
976
|
+
const toRawPost = (post: PostView, feed?: FeedContext) =>
|
|
977
|
+
Effect.gen(function* () {
|
|
978
|
+
const embed = yield* mapEmbedView(post.embed);
|
|
979
|
+
const authorProfile = yield* decodeProfileBasic(post.author);
|
|
980
|
+
const raw = {
|
|
981
|
+
uri: post.uri,
|
|
982
|
+
cid: post.cid,
|
|
983
|
+
author: post.author.handle,
|
|
984
|
+
authorDid: post.author.did,
|
|
985
|
+
authorProfile,
|
|
986
|
+
record: post.record,
|
|
987
|
+
indexedAt: post.indexedAt,
|
|
988
|
+
labels: post.labels,
|
|
989
|
+
metrics: metricsFromPostView(post),
|
|
990
|
+
embed,
|
|
991
|
+
viewer: post.viewer,
|
|
992
|
+
threadgate: post.threadgate,
|
|
993
|
+
debug: post.debug,
|
|
994
|
+
feed
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
return yield* Schema.decodeUnknown(RawPost)(raw).pipe(
|
|
998
|
+
Effect.mapError(toBskyError("Invalid post payload"))
|
|
999
|
+
);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const toRawPostsFromFeed = (feed: ReadonlyArray<FeedViewPost>) =>
|
|
1003
|
+
Effect.partition(feed, (item, index) =>
|
|
1004
|
+
Effect.gen(function* () {
|
|
1005
|
+
const context = yield* mapFeedContext(item);
|
|
1006
|
+
return yield* toRawPost(item.post, context);
|
|
1007
|
+
}).pipe(
|
|
1008
|
+
Effect.tapError((error) =>
|
|
1009
|
+
Effect.logWarning(`Skipping malformed feed item at index ${index}`, {
|
|
1010
|
+
uri: item.post?.uri,
|
|
1011
|
+
error: error.message
|
|
1012
|
+
})
|
|
1013
|
+
)
|
|
1014
|
+
)
|
|
1015
|
+
).pipe(
|
|
1016
|
+
Effect.tap(([errors, successes]) => {
|
|
1017
|
+
if (errors.length > 0) {
|
|
1018
|
+
return Effect.log(
|
|
1019
|
+
`Feed sync: processed ${successes.length} posts, skipped ${errors.length} malformed items`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
return Effect.void;
|
|
1023
|
+
}),
|
|
1024
|
+
Effect.map(([_, successes]) => successes)
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
const chunkArray = <A>(
|
|
1028
|
+
items: ReadonlyArray<A>,
|
|
1029
|
+
size: number
|
|
1030
|
+
): Array<Array<A>> => {
|
|
1031
|
+
if (items.length === 0) return [];
|
|
1032
|
+
const chunkSize = Math.max(1, Math.trunc(size));
|
|
1033
|
+
const chunks: Array<Array<A>> = [];
|
|
1034
|
+
for (let index = 0; index < items.length; index += chunkSize) {
|
|
1035
|
+
chunks.push(items.slice(index, index + chunkSize));
|
|
1036
|
+
}
|
|
1037
|
+
return chunks;
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
const isPostRecord = (record: unknown): record is AppBskyFeedPost.Record =>
|
|
1041
|
+
typeof record === "object" &&
|
|
1042
|
+
record !== null &&
|
|
1043
|
+
(record as { $type?: unknown }).$type === "app.bsky.feed.post";
|
|
1044
|
+
|
|
1045
|
+
const collectThreadChildren = (node: ThreadViewPost): ReadonlyArray<ThreadViewPost> => {
|
|
1046
|
+
const items: Array<ThreadViewPost> = [];
|
|
1047
|
+
const parent = node.parent;
|
|
1048
|
+
if (parent && AppBskyFeedDefs.isThreadViewPost(parent)) {
|
|
1049
|
+
items.push(parent);
|
|
1050
|
+
}
|
|
1051
|
+
if (Array.isArray(node.replies)) {
|
|
1052
|
+
for (const reply of node.replies) {
|
|
1053
|
+
if (AppBskyFeedDefs.isThreadViewPost(reply)) {
|
|
1054
|
+
items.push(reply);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return items;
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const unfoldThread = (root: ThreadViewPost) =>
|
|
1062
|
+
Stream.unfoldEffect(
|
|
1063
|
+
{ queue: [root] as ReadonlyArray<ThreadViewPost>, seen: new Set<string>() },
|
|
1064
|
+
(state) =>
|
|
1065
|
+
Effect.sync(() => {
|
|
1066
|
+
const queue = state.queue.slice();
|
|
1067
|
+
while (queue.length > 0) {
|
|
1068
|
+
const next = queue.shift()!;
|
|
1069
|
+
const nextUri = next.post?.uri;
|
|
1070
|
+
const children = collectThreadChildren(next);
|
|
1071
|
+
queue.push(...children);
|
|
1072
|
+
if (typeof nextUri === "string") {
|
|
1073
|
+
if (state.seen.has(nextUri)) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
state.seen.add(nextUri);
|
|
1077
|
+
}
|
|
1078
|
+
return Option.some([next, { queue, seen: state.seen }] as const);
|
|
1079
|
+
}
|
|
1080
|
+
return Option.none();
|
|
1081
|
+
})
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
const toRawPostsFromThread = (root: ThreadViewPost) =>
|
|
1085
|
+
unfoldThread(root).pipe(
|
|
1086
|
+
Stream.mapEffect((node) => toRawPost(node.post)),
|
|
1087
|
+
Stream.runCollect,
|
|
1088
|
+
Effect.map(Chunk.toReadonlyArray)
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Service for interacting with the Bluesky (AT Protocol) API.
|
|
1093
|
+
*
|
|
1094
|
+
* Provides authenticated access to all major Bluesky API endpoints including:
|
|
1095
|
+
* - Timeline and feed retrieval
|
|
1096
|
+
* - Post search and discovery
|
|
1097
|
+
* - Social graph operations (followers, follows, lists)
|
|
1098
|
+
* - Engagement data (likes, reposts, quotes)
|
|
1099
|
+
* - Notifications
|
|
1100
|
+
* - Thread viewing
|
|
1101
|
+
*
|
|
1102
|
+
* ## Authentication
|
|
1103
|
+
*
|
|
1104
|
+
* The client automatically handles authentication using credentials resolved
|
|
1105
|
+
* from environment variables, credential store, or interactive prompts.
|
|
1106
|
+
*
|
|
1107
|
+
* ## Error Handling
|
|
1108
|
+
*
|
|
1109
|
+
* All methods return Effect values that can fail with `BskyError`. Common
|
|
1110
|
+
* error scenarios include:
|
|
1111
|
+
* - Network failures (automatically retried)
|
|
1112
|
+
* - Rate limiting (automatically retried with backoff)
|
|
1113
|
+
* - Authentication errors
|
|
1114
|
+
* - Invalid post/feed URIs
|
|
1115
|
+
*
|
|
1116
|
+
* ## Streaming
|
|
1117
|
+
*
|
|
1118
|
+
* Paginated endpoints (timeline, feeds, search) return `Stream.Stream` values
|
|
1119
|
+
* that automatically handle pagination. Use `Effect.runCollect` to gather
|
|
1120
|
+
* all results or process them incrementally.
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```ts
|
|
1124
|
+
* import { Effect, Stream } from "effect";
|
|
1125
|
+
* import { BskyClient } from "./services/bsky-client.js";
|
|
1126
|
+
*
|
|
1127
|
+
* const program = Effect.gen(function* () {
|
|
1128
|
+
* const client = yield* BskyClient;
|
|
1129
|
+
*
|
|
1130
|
+
* // Get timeline as a stream
|
|
1131
|
+
* const timelineStream = client.getTimeline({ limit: 100 });
|
|
1132
|
+
*
|
|
1133
|
+
* // Collect all posts
|
|
1134
|
+
* const allPosts = yield* timelineStream.pipe(Stream.runCollect);
|
|
1135
|
+
*
|
|
1136
|
+
* // Get a specific post
|
|
1137
|
+
* const post = yield* client.getPost("at://did:plc:abc/app.bsky.feed.post/123");
|
|
1138
|
+
*
|
|
1139
|
+
* // Search for posts
|
|
1140
|
+
* const results = yield* client.searchPosts("typescript", {
|
|
1141
|
+
* sort: "latest",
|
|
1142
|
+
* limit: 50
|
|
1143
|
+
* }).pipe(Stream.runCollect);
|
|
1144
|
+
* });
|
|
1145
|
+
* ```
|
|
1146
|
+
*/
|
|
1147
|
+
export class BskyClient extends Context.Tag("@skygent/BskyClient")<
|
|
1148
|
+
BskyClient,
|
|
1149
|
+
{
|
|
1150
|
+
/**
|
|
1151
|
+
* Get the authenticated user's home timeline.
|
|
1152
|
+
*
|
|
1153
|
+
* Returns a stream of posts from followed accounts.
|
|
1154
|
+
*
|
|
1155
|
+
* @param opts - Pagination options
|
|
1156
|
+
* @returns Stream of posts from the timeline
|
|
1157
|
+
*/
|
|
1158
|
+
readonly getTimeline: (opts?: TimelineOptions) => Stream.Stream<RawPost, BskyError>;
|
|
1159
|
+
readonly getNotifications: (
|
|
1160
|
+
opts?: NotificationsOptions
|
|
1161
|
+
) => Stream.Stream<RawPost, BskyError>;
|
|
1162
|
+
readonly getFeed: (
|
|
1163
|
+
uri: string,
|
|
1164
|
+
opts?: FeedOptions
|
|
1165
|
+
) => Stream.Stream<RawPost, BskyError>;
|
|
1166
|
+
readonly getListFeed: (
|
|
1167
|
+
uri: string,
|
|
1168
|
+
opts?: FeedOptions
|
|
1169
|
+
) => Stream.Stream<RawPost, BskyError>;
|
|
1170
|
+
readonly getAuthorFeed: (
|
|
1171
|
+
actor: string,
|
|
1172
|
+
opts?: AuthorFeedOptions
|
|
1173
|
+
) => Stream.Stream<RawPost, BskyError>;
|
|
1174
|
+
readonly getPost: (uri: string) => Effect.Effect<RawPost, BskyError>;
|
|
1175
|
+
readonly getPostThread: (
|
|
1176
|
+
uri: string,
|
|
1177
|
+
opts?: ThreadOptions
|
|
1178
|
+
) => Effect.Effect<ReadonlyArray<RawPost>, BskyError>;
|
|
1179
|
+
readonly getFollowers: (
|
|
1180
|
+
actor: string,
|
|
1181
|
+
opts?: GraphOptions
|
|
1182
|
+
) => Effect.Effect<{ readonly subject: ProfileView; readonly followers: ReadonlyArray<ProfileView>; readonly cursor?: string }, BskyError>;
|
|
1183
|
+
readonly getFollows: (
|
|
1184
|
+
actor: string,
|
|
1185
|
+
opts?: GraphOptions
|
|
1186
|
+
) => Effect.Effect<{ readonly subject: ProfileView; readonly follows: ReadonlyArray<ProfileView>; readonly cursor?: string }, BskyError>;
|
|
1187
|
+
readonly getKnownFollowers: (
|
|
1188
|
+
actor: string,
|
|
1189
|
+
opts?: GraphOptions
|
|
1190
|
+
) => Effect.Effect<{ readonly subject: ProfileView; readonly followers: ReadonlyArray<ProfileView>; readonly cursor?: string }, BskyError>;
|
|
1191
|
+
readonly getRelationships: (
|
|
1192
|
+
actor: string,
|
|
1193
|
+
others: ReadonlyArray<string>
|
|
1194
|
+
) => Effect.Effect<{ readonly actor: string; readonly relationships: ReadonlyArray<RelationshipView> }, BskyError>;
|
|
1195
|
+
readonly getList: (
|
|
1196
|
+
uri: string,
|
|
1197
|
+
opts?: GraphOptions
|
|
1198
|
+
) => Effect.Effect<{ readonly list: ListView; readonly items: ReadonlyArray<ListItemView>; readonly cursor?: string }, BskyError>;
|
|
1199
|
+
readonly getLists: (
|
|
1200
|
+
actor: string,
|
|
1201
|
+
opts?: GraphListsOptions
|
|
1202
|
+
) => Effect.Effect<{ readonly lists: ReadonlyArray<ListView>; readonly cursor?: string }, BskyError>;
|
|
1203
|
+
readonly getBlocks: (
|
|
1204
|
+
opts?: GraphOptions
|
|
1205
|
+
) => Effect.Effect<{ readonly blocks: ReadonlyArray<ProfileView>; readonly cursor?: string }, BskyError>;
|
|
1206
|
+
readonly getMutes: (
|
|
1207
|
+
opts?: GraphOptions
|
|
1208
|
+
) => Effect.Effect<{ readonly mutes: ReadonlyArray<ProfileView>; readonly cursor?: string }, BskyError>;
|
|
1209
|
+
readonly getFeedGenerator: (
|
|
1210
|
+
uri: string
|
|
1211
|
+
) => Effect.Effect<{ readonly view: FeedGeneratorView; readonly isOnline: boolean; readonly isValid: boolean }, BskyError>;
|
|
1212
|
+
readonly getFeedGenerators: (
|
|
1213
|
+
uris: ReadonlyArray<string>
|
|
1214
|
+
) => Effect.Effect<{ readonly feeds: ReadonlyArray<FeedGeneratorView> }, BskyError>;
|
|
1215
|
+
readonly getActorFeeds: (
|
|
1216
|
+
actor: string,
|
|
1217
|
+
opts?: ActorFeedsOptions
|
|
1218
|
+
) => Effect.Effect<{ readonly feeds: ReadonlyArray<FeedGeneratorView>; readonly cursor?: string }, BskyError>;
|
|
1219
|
+
readonly getLikes: (
|
|
1220
|
+
uri: string,
|
|
1221
|
+
opts?: EngagementOptions
|
|
1222
|
+
) => Effect.Effect<{ readonly uri: string; readonly cid?: string; readonly likes: ReadonlyArray<PostLike>; readonly cursor?: string }, BskyError>;
|
|
1223
|
+
readonly getRepostedBy: (
|
|
1224
|
+
uri: string,
|
|
1225
|
+
opts?: EngagementOptions
|
|
1226
|
+
) => Effect.Effect<{ readonly uri: string; readonly cid?: string; readonly repostedBy: ReadonlyArray<ProfileView>; readonly cursor?: string }, BskyError>;
|
|
1227
|
+
readonly getQuotes: (
|
|
1228
|
+
uri: string,
|
|
1229
|
+
opts?: EngagementOptions
|
|
1230
|
+
) => Effect.Effect<{ readonly uri: string; readonly cid?: string; readonly posts: ReadonlyArray<RawPost>; readonly cursor?: string }, BskyError>;
|
|
1231
|
+
readonly resolveHandle: (handle: string) => Effect.Effect<Did, BskyError>;
|
|
1232
|
+
readonly resolveIdentity: (
|
|
1233
|
+
identifier: string
|
|
1234
|
+
) => Effect.Effect<IdentityInfo, BskyError>;
|
|
1235
|
+
readonly getProfiles: (
|
|
1236
|
+
actors: ReadonlyArray<string>
|
|
1237
|
+
) => Effect.Effect<ReadonlyArray<ProfileBasic>, BskyError>;
|
|
1238
|
+
readonly searchActors: (
|
|
1239
|
+
query: string,
|
|
1240
|
+
opts?: ActorSearchOptions
|
|
1241
|
+
) => Effect.Effect<{ readonly actors: ReadonlyArray<ProfileView>; readonly cursor?: string }, BskyError>;
|
|
1242
|
+
readonly searchFeedGenerators: (
|
|
1243
|
+
query: string,
|
|
1244
|
+
opts?: FeedSearchOptions
|
|
1245
|
+
) => Effect.Effect<{ readonly feeds: ReadonlyArray<FeedGeneratorView>; readonly cursor?: string }, BskyError>;
|
|
1246
|
+
readonly searchPosts: (
|
|
1247
|
+
query: string,
|
|
1248
|
+
opts?: NetworkSearchOptions
|
|
1249
|
+
) => Effect.Effect<{ readonly posts: ReadonlyArray<RawPost>; readonly cursor?: string; readonly hitsTotal?: number }, BskyError>;
|
|
1250
|
+
readonly getTrendingTopics: () => Effect.Effect<ReadonlyArray<string>, BskyError>;
|
|
1251
|
+
}
|
|
1252
|
+
>() {
|
|
1253
|
+
static readonly layer = Layer.effect(
|
|
1254
|
+
BskyClient,
|
|
1255
|
+
Effect.gen(function* () {
|
|
1256
|
+
const config = yield* AppConfigService;
|
|
1257
|
+
const credentials = yield* CredentialStore;
|
|
1258
|
+
const agent = new AtpAgent({ service: config.service });
|
|
1259
|
+
|
|
1260
|
+
const minInterval = yield* Config.duration("SKYGENT_BSKY_RATE_LIMIT").pipe(
|
|
1261
|
+
Config.withDefault(Duration.millis(250))
|
|
1262
|
+
);
|
|
1263
|
+
const retryBase = yield* Config.duration("SKYGENT_BSKY_RETRY_BASE").pipe(
|
|
1264
|
+
Config.withDefault(Duration.millis(250))
|
|
1265
|
+
);
|
|
1266
|
+
const retryMax = yield* Config.integer("SKYGENT_BSKY_RETRY_MAX").pipe(
|
|
1267
|
+
Config.withDefault(5)
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
const limiter = yield* Effect.makeSemaphore(1);
|
|
1271
|
+
const lastCallRef = yield* Ref.make(0);
|
|
1272
|
+
const minIntervalMs = Duration.toMillis(minInterval);
|
|
1273
|
+
|
|
1274
|
+
const withRateLimit = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
1275
|
+
limiter.withPermits(1)(
|
|
1276
|
+
Effect.gen(function* () {
|
|
1277
|
+
const now = yield* Clock.currentTimeMillis;
|
|
1278
|
+
const last = yield* Ref.get(lastCallRef);
|
|
1279
|
+
const waitMs = Math.max(0, minIntervalMs - (now - last));
|
|
1280
|
+
if (waitMs > 0) {
|
|
1281
|
+
yield* Effect.sleep(Duration.millis(waitMs));
|
|
1282
|
+
}
|
|
1283
|
+
return yield* effect;
|
|
1284
|
+
}).pipe(
|
|
1285
|
+
Effect.ensuring(
|
|
1286
|
+
Clock.currentTimeMillis.pipe(
|
|
1287
|
+
Effect.flatMap((now) => Ref.set(lastCallRef, now))
|
|
1288
|
+
)
|
|
1289
|
+
)
|
|
1290
|
+
)
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
const retrySchedule = Schedule.exponential(retryBase).pipe(
|
|
1294
|
+
Schedule.jittered,
|
|
1295
|
+
Schedule.intersect(Schedule.recurWhile(isRetryableCause)),
|
|
1296
|
+
Schedule.intersect(Schedule.recurs(retryMax))
|
|
1297
|
+
);
|
|
1298
|
+
|
|
1299
|
+
const withRetry = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
1300
|
+
effect.pipe(Effect.retry(retrySchedule));
|
|
1301
|
+
|
|
1302
|
+
const ensureAuth = (required: boolean) =>
|
|
1303
|
+
Effect.gen(function* () {
|
|
1304
|
+
if (agent.hasSession) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const creds = yield* credentials
|
|
1308
|
+
.get()
|
|
1309
|
+
.pipe(Effect.mapError(toBskyError("Failed to load credentials", "loadCredentials")));
|
|
1310
|
+
if (Option.isNone(creds)) {
|
|
1311
|
+
if (required) {
|
|
1312
|
+
return yield* BskyError.make({
|
|
1313
|
+
message:
|
|
1314
|
+
"Missing Bluesky credentials. Provide identifier and password."
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const value = creds.value;
|
|
1320
|
+
yield* withRetry(
|
|
1321
|
+
withRateLimit(
|
|
1322
|
+
Effect.tryPromise(() =>
|
|
1323
|
+
agent.login({
|
|
1324
|
+
identifier: value.identifier,
|
|
1325
|
+
password: Redacted.value(value.password)
|
|
1326
|
+
})
|
|
1327
|
+
)
|
|
1328
|
+
)
|
|
1329
|
+
).pipe(Effect.mapError(toBskyError("Bluesky login failed", "login")));
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
const paginate = <A>(
|
|
1333
|
+
initialCursor: string | undefined,
|
|
1334
|
+
fetch: (cursor: string | undefined) => Effect.Effect<
|
|
1335
|
+
readonly [Chunk.Chunk<A>, Option.Option<string>],
|
|
1336
|
+
BskyError
|
|
1337
|
+
>
|
|
1338
|
+
) => Stream.paginateChunkEffect(initialCursor, fetch);
|
|
1339
|
+
|
|
1340
|
+
const getTimeline = (opts?: TimelineOptions) =>
|
|
1341
|
+
paginate(opts?.cursor, (cursor) =>
|
|
1342
|
+
Effect.gen(function* () {
|
|
1343
|
+
yield* ensureAuth(true);
|
|
1344
|
+
const params = withCursor(
|
|
1345
|
+
{ limit: opts?.limit ?? 50 },
|
|
1346
|
+
cursor
|
|
1347
|
+
);
|
|
1348
|
+
const response = yield* withRetry(
|
|
1349
|
+
withRateLimit(
|
|
1350
|
+
Effect.tryPromise<AppBskyFeedGetTimeline.Response>(() =>
|
|
1351
|
+
agent.app.bsky.feed.getTimeline(params)
|
|
1352
|
+
)
|
|
1353
|
+
)
|
|
1354
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch timeline", "getTimeline")));
|
|
1355
|
+
const posts = yield* toRawPostsFromFeed(response.data.feed);
|
|
1356
|
+
const nextCursor = response.data.cursor;
|
|
1357
|
+
const tagged = posts.map((p) => new RawPost({ ...p, _pageCursor: nextCursor }));
|
|
1358
|
+
const hasNext =
|
|
1359
|
+
tagged.length > 0 &&
|
|
1360
|
+
typeof nextCursor === "string" &&
|
|
1361
|
+
nextCursor !== cursor;
|
|
1362
|
+
return [
|
|
1363
|
+
Chunk.fromIterable(tagged),
|
|
1364
|
+
hasNext ? Option.some(nextCursor) : Option.none()
|
|
1365
|
+
] as const;
|
|
1366
|
+
})
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
const getFeed = (uri: string, opts?: FeedOptions) =>
|
|
1370
|
+
paginate(opts?.cursor, (cursor) =>
|
|
1371
|
+
Effect.gen(function* () {
|
|
1372
|
+
yield* ensureAuth(false);
|
|
1373
|
+
const params = withCursor(
|
|
1374
|
+
{ feed: uri, limit: opts?.limit ?? 50 },
|
|
1375
|
+
cursor
|
|
1376
|
+
);
|
|
1377
|
+
const response = yield* withRetry(
|
|
1378
|
+
withRateLimit(
|
|
1379
|
+
Effect.tryPromise<AppBskyFeedGetFeed.Response>(() =>
|
|
1380
|
+
agent.app.bsky.feed.getFeed(params)
|
|
1381
|
+
)
|
|
1382
|
+
)
|
|
1383
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch feed", "getFeed")));
|
|
1384
|
+
const posts = yield* toRawPostsFromFeed(response.data.feed);
|
|
1385
|
+
const nextCursor = response.data.cursor;
|
|
1386
|
+
const tagged = posts.map((p) => new RawPost({ ...p, _pageCursor: nextCursor }));
|
|
1387
|
+
const hasNext =
|
|
1388
|
+
tagged.length > 0 &&
|
|
1389
|
+
typeof nextCursor === "string" &&
|
|
1390
|
+
nextCursor !== cursor;
|
|
1391
|
+
return [
|
|
1392
|
+
Chunk.fromIterable(tagged),
|
|
1393
|
+
hasNext ? Option.some(nextCursor) : Option.none()
|
|
1394
|
+
] as const;
|
|
1395
|
+
})
|
|
1396
|
+
);
|
|
1397
|
+
|
|
1398
|
+
const getListFeed = (uri: string, opts?: FeedOptions) =>
|
|
1399
|
+
paginate(opts?.cursor, (cursor) =>
|
|
1400
|
+
Effect.gen(function* () {
|
|
1401
|
+
yield* ensureAuth(false);
|
|
1402
|
+
const params = withCursor(
|
|
1403
|
+
{ list: uri, limit: opts?.limit ?? 50 },
|
|
1404
|
+
cursor
|
|
1405
|
+
);
|
|
1406
|
+
const response = yield* withRetry(
|
|
1407
|
+
withRateLimit(
|
|
1408
|
+
Effect.tryPromise<AppBskyFeedGetListFeed.Response>(() =>
|
|
1409
|
+
agent.app.bsky.feed.getListFeed(params)
|
|
1410
|
+
)
|
|
1411
|
+
)
|
|
1412
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch list feed", "getListFeed")));
|
|
1413
|
+
const posts = yield* toRawPostsFromFeed(response.data.feed);
|
|
1414
|
+
const nextCursor = response.data.cursor;
|
|
1415
|
+
const tagged = posts.map((p) => new RawPost({ ...p, _pageCursor: nextCursor }));
|
|
1416
|
+
const hasNext =
|
|
1417
|
+
tagged.length > 0 &&
|
|
1418
|
+
typeof nextCursor === "string" &&
|
|
1419
|
+
nextCursor !== cursor;
|
|
1420
|
+
return [
|
|
1421
|
+
Chunk.fromIterable(tagged),
|
|
1422
|
+
hasNext ? Option.some(nextCursor) : Option.none()
|
|
1423
|
+
] as const;
|
|
1424
|
+
})
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
const getAuthorFeed = (actor: string, opts?: AuthorFeedOptions) =>
|
|
1428
|
+
paginate(opts?.cursor, (cursor) =>
|
|
1429
|
+
Effect.gen(function* () {
|
|
1430
|
+
yield* ensureAuth(false);
|
|
1431
|
+
const includePins = opts?.includePins;
|
|
1432
|
+
const params = withCursor(
|
|
1433
|
+
{
|
|
1434
|
+
actor,
|
|
1435
|
+
limit: opts?.limit ?? 50,
|
|
1436
|
+
...(opts?.filter ? { filter: opts.filter } : {}),
|
|
1437
|
+
...(includePins !== undefined ? { includePins } : {})
|
|
1438
|
+
},
|
|
1439
|
+
cursor
|
|
1440
|
+
);
|
|
1441
|
+
const response = yield* withRetry(
|
|
1442
|
+
withRateLimit(
|
|
1443
|
+
Effect.tryPromise<AppBskyFeedGetAuthorFeed.Response>(() =>
|
|
1444
|
+
agent.app.bsky.feed.getAuthorFeed(params)
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1447
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch author feed", "getAuthorFeed")));
|
|
1448
|
+
const posts = yield* toRawPostsFromFeed(response.data.feed);
|
|
1449
|
+
const nextCursor = response.data.cursor;
|
|
1450
|
+
const tagged = posts.map((p) => new RawPost({ ...p, _pageCursor: nextCursor }));
|
|
1451
|
+
const hasNext =
|
|
1452
|
+
tagged.length > 0 &&
|
|
1453
|
+
typeof nextCursor === "string" &&
|
|
1454
|
+
nextCursor !== cursor;
|
|
1455
|
+
return [
|
|
1456
|
+
Chunk.fromIterable(tagged),
|
|
1457
|
+
hasNext ? Option.some(nextCursor) : Option.none()
|
|
1458
|
+
] as const;
|
|
1459
|
+
})
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
const getPost = (uri: string) =>
|
|
1463
|
+
Effect.gen(function* () {
|
|
1464
|
+
yield* ensureAuth(false);
|
|
1465
|
+
const response = yield* withRetry(
|
|
1466
|
+
withRateLimit(
|
|
1467
|
+
Effect.tryPromise<AppBskyFeedGetPosts.Response>(() =>
|
|
1468
|
+
agent.app.bsky.feed.getPosts({ uris: [uri] })
|
|
1469
|
+
)
|
|
1470
|
+
)
|
|
1471
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch post", "getPosts")));
|
|
1472
|
+
const postView = response.data.posts[0];
|
|
1473
|
+
if (!postView) {
|
|
1474
|
+
return yield* BskyError.make({
|
|
1475
|
+
message: "Post not found",
|
|
1476
|
+
cause: uri
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
return yield* toRawPost(postView);
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
const getPostThread = (uri: string, opts?: ThreadOptions) =>
|
|
1483
|
+
Effect.gen(function* () {
|
|
1484
|
+
yield* ensureAuth(false);
|
|
1485
|
+
const params = {
|
|
1486
|
+
uri,
|
|
1487
|
+
...(opts?.depth !== undefined ? { depth: opts.depth } : {}),
|
|
1488
|
+
...(opts?.parentHeight !== undefined
|
|
1489
|
+
? { parentHeight: opts.parentHeight }
|
|
1490
|
+
: {})
|
|
1491
|
+
};
|
|
1492
|
+
const response = yield* withRetry(
|
|
1493
|
+
withRateLimit(
|
|
1494
|
+
Effect.tryPromise<AppBskyFeedGetPostThread.Response>(() =>
|
|
1495
|
+
agent.app.bsky.feed.getPostThread(params)
|
|
1496
|
+
)
|
|
1497
|
+
)
|
|
1498
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch post thread", "getPostThread")));
|
|
1499
|
+
|
|
1500
|
+
if (!AppBskyFeedDefs.isThreadViewPost(response.data.thread)) {
|
|
1501
|
+
return [] as ReadonlyArray<RawPost>;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return yield* toRawPostsFromThread(response.data.thread);
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
const getFollowers = (actor: string, opts?: GraphOptions) =>
|
|
1508
|
+
Effect.gen(function* () {
|
|
1509
|
+
yield* ensureAuth(false);
|
|
1510
|
+
const params = withCursor(
|
|
1511
|
+
{ actor, limit: opts?.limit ?? 50 },
|
|
1512
|
+
opts?.cursor
|
|
1513
|
+
);
|
|
1514
|
+
const response = yield* withRetry(
|
|
1515
|
+
withRateLimit(
|
|
1516
|
+
Effect.tryPromise<AppBskyGraphGetFollowers.Response>(() =>
|
|
1517
|
+
agent.app.bsky.graph.getFollowers(params)
|
|
1518
|
+
)
|
|
1519
|
+
)
|
|
1520
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch followers", "getFollowers")));
|
|
1521
|
+
const subject = yield* decodeProfileView(response.data.subject);
|
|
1522
|
+
const followers = yield* Effect.forEach(
|
|
1523
|
+
response.data.followers,
|
|
1524
|
+
decodeProfileView,
|
|
1525
|
+
{ concurrency: "unbounded" }
|
|
1526
|
+
);
|
|
1527
|
+
const cursor = response.data.cursor;
|
|
1528
|
+
return cursor ? { subject, followers, cursor } : { subject, followers };
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
const getFollows = (actor: string, opts?: GraphOptions) =>
|
|
1532
|
+
Effect.gen(function* () {
|
|
1533
|
+
yield* ensureAuth(false);
|
|
1534
|
+
const params = withCursor(
|
|
1535
|
+
{ actor, limit: opts?.limit ?? 50 },
|
|
1536
|
+
opts?.cursor
|
|
1537
|
+
);
|
|
1538
|
+
const response = yield* withRetry(
|
|
1539
|
+
withRateLimit(
|
|
1540
|
+
Effect.tryPromise<AppBskyGraphGetFollows.Response>(() =>
|
|
1541
|
+
agent.app.bsky.graph.getFollows(params)
|
|
1542
|
+
)
|
|
1543
|
+
)
|
|
1544
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch follows", "getFollows")));
|
|
1545
|
+
const subject = yield* decodeProfileView(response.data.subject);
|
|
1546
|
+
const follows = yield* Effect.forEach(
|
|
1547
|
+
response.data.follows,
|
|
1548
|
+
decodeProfileView,
|
|
1549
|
+
{ concurrency: "unbounded" }
|
|
1550
|
+
);
|
|
1551
|
+
const cursor = response.data.cursor;
|
|
1552
|
+
return cursor ? { subject, follows, cursor } : { subject, follows };
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
const getKnownFollowers = (actor: string, opts?: GraphOptions) =>
|
|
1556
|
+
Effect.gen(function* () {
|
|
1557
|
+
yield* ensureAuth(true);
|
|
1558
|
+
const params = withCursor(
|
|
1559
|
+
{ actor, limit: opts?.limit ?? 50 },
|
|
1560
|
+
opts?.cursor
|
|
1561
|
+
);
|
|
1562
|
+
const response = yield* withRetry(
|
|
1563
|
+
withRateLimit(
|
|
1564
|
+
Effect.tryPromise<AppBskyGraphGetKnownFollowers.Response>(() =>
|
|
1565
|
+
agent.app.bsky.graph.getKnownFollowers(params)
|
|
1566
|
+
)
|
|
1567
|
+
)
|
|
1568
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch known followers", "getKnownFollowers")));
|
|
1569
|
+
const subject = yield* decodeProfileView(response.data.subject);
|
|
1570
|
+
const followers = yield* Effect.forEach(
|
|
1571
|
+
response.data.followers,
|
|
1572
|
+
decodeProfileView,
|
|
1573
|
+
{ concurrency: "unbounded" }
|
|
1574
|
+
);
|
|
1575
|
+
const cursor = response.data.cursor;
|
|
1576
|
+
return cursor ? { subject, followers, cursor } : { subject, followers };
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
const getRelationships = (actor: string, others: ReadonlyArray<string>) =>
|
|
1580
|
+
Effect.gen(function* () {
|
|
1581
|
+
if (others.length === 0) {
|
|
1582
|
+
return { actor, relationships: [] };
|
|
1583
|
+
}
|
|
1584
|
+
yield* ensureAuth(false);
|
|
1585
|
+
const response = yield* withRetry(
|
|
1586
|
+
withRateLimit(
|
|
1587
|
+
Effect.tryPromise<AppBskyGraphGetRelationships.Response>(() =>
|
|
1588
|
+
agent.app.bsky.graph.getRelationships({ actor, others: [...others] })
|
|
1589
|
+
)
|
|
1590
|
+
)
|
|
1591
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch relationships", "getRelationships")));
|
|
1592
|
+
const relationships = yield* Effect.forEach(
|
|
1593
|
+
response.data.relationships,
|
|
1594
|
+
decodeRelationshipView,
|
|
1595
|
+
{ concurrency: "unbounded" }
|
|
1596
|
+
);
|
|
1597
|
+
const actorDid =
|
|
1598
|
+
typeof response.data.actor === "string" ? response.data.actor : actor;
|
|
1599
|
+
return { actor: actorDid, relationships };
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
const getList = (uri: string, opts?: GraphOptions) =>
|
|
1603
|
+
Effect.gen(function* () {
|
|
1604
|
+
yield* ensureAuth(false);
|
|
1605
|
+
const params = withCursor(
|
|
1606
|
+
{ list: uri, limit: opts?.limit ?? 50 },
|
|
1607
|
+
opts?.cursor
|
|
1608
|
+
);
|
|
1609
|
+
const response = yield* withRetry(
|
|
1610
|
+
withRateLimit(
|
|
1611
|
+
Effect.tryPromise<AppBskyGraphGetList.Response>(() =>
|
|
1612
|
+
agent.app.bsky.graph.getList(params)
|
|
1613
|
+
)
|
|
1614
|
+
)
|
|
1615
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch list", "getList")));
|
|
1616
|
+
const list = yield* decodeListView(response.data.list);
|
|
1617
|
+
const items = yield* Effect.forEach(
|
|
1618
|
+
response.data.items,
|
|
1619
|
+
decodeListItemView,
|
|
1620
|
+
{ concurrency: "unbounded" }
|
|
1621
|
+
);
|
|
1622
|
+
const cursor = response.data.cursor;
|
|
1623
|
+
return cursor ? { list, items, cursor } : { list, items };
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
const getLists = (actor: string, opts?: GraphListsOptions) =>
|
|
1627
|
+
Effect.gen(function* () {
|
|
1628
|
+
yield* ensureAuth(false);
|
|
1629
|
+
const params = withCursor(
|
|
1630
|
+
{
|
|
1631
|
+
actor,
|
|
1632
|
+
limit: opts?.limit ?? 50,
|
|
1633
|
+
...(opts?.purposes && opts.purposes.length > 0
|
|
1634
|
+
? { purposes: [...opts.purposes] }
|
|
1635
|
+
: {})
|
|
1636
|
+
},
|
|
1637
|
+
opts?.cursor
|
|
1638
|
+
);
|
|
1639
|
+
const response = yield* withRetry(
|
|
1640
|
+
withRateLimit(
|
|
1641
|
+
Effect.tryPromise<AppBskyGraphGetLists.Response>(() =>
|
|
1642
|
+
agent.app.bsky.graph.getLists(params)
|
|
1643
|
+
)
|
|
1644
|
+
)
|
|
1645
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch lists", "getLists")));
|
|
1646
|
+
const lists = yield* Effect.forEach(
|
|
1647
|
+
response.data.lists,
|
|
1648
|
+
decodeListView,
|
|
1649
|
+
{ concurrency: "unbounded" }
|
|
1650
|
+
);
|
|
1651
|
+
const cursor = response.data.cursor;
|
|
1652
|
+
return cursor ? { lists, cursor } : { lists };
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
const getBlocks = (opts?: GraphOptions) =>
|
|
1656
|
+
Effect.gen(function* () {
|
|
1657
|
+
yield* ensureAuth(true);
|
|
1658
|
+
const params = withCursor(
|
|
1659
|
+
{ limit: opts?.limit ?? 50 },
|
|
1660
|
+
opts?.cursor
|
|
1661
|
+
);
|
|
1662
|
+
const response = yield* withRetry(
|
|
1663
|
+
withRateLimit(
|
|
1664
|
+
Effect.tryPromise<AppBskyGraphGetBlocks.Response>(() =>
|
|
1665
|
+
agent.app.bsky.graph.getBlocks(params)
|
|
1666
|
+
)
|
|
1667
|
+
)
|
|
1668
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch blocks", "getBlocks")));
|
|
1669
|
+
const blocks = yield* Effect.forEach(
|
|
1670
|
+
response.data.blocks,
|
|
1671
|
+
decodeProfileView,
|
|
1672
|
+
{ concurrency: "unbounded" }
|
|
1673
|
+
);
|
|
1674
|
+
const cursor = response.data.cursor;
|
|
1675
|
+
return cursor ? { blocks, cursor } : { blocks };
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
const getMutes = (opts?: GraphOptions) =>
|
|
1679
|
+
Effect.gen(function* () {
|
|
1680
|
+
yield* ensureAuth(true);
|
|
1681
|
+
const params = withCursor(
|
|
1682
|
+
{ limit: opts?.limit ?? 50 },
|
|
1683
|
+
opts?.cursor
|
|
1684
|
+
);
|
|
1685
|
+
const response = yield* withRetry(
|
|
1686
|
+
withRateLimit(
|
|
1687
|
+
Effect.tryPromise<AppBskyGraphGetMutes.Response>(() =>
|
|
1688
|
+
agent.app.bsky.graph.getMutes(params)
|
|
1689
|
+
)
|
|
1690
|
+
)
|
|
1691
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch mutes", "getMutes")));
|
|
1692
|
+
const mutes = yield* Effect.forEach(
|
|
1693
|
+
response.data.mutes,
|
|
1694
|
+
decodeProfileView,
|
|
1695
|
+
{ concurrency: "unbounded" }
|
|
1696
|
+
);
|
|
1697
|
+
const cursor = response.data.cursor;
|
|
1698
|
+
return cursor ? { mutes, cursor } : { mutes };
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
const getFeedGenerator = (uri: string) =>
|
|
1702
|
+
Effect.gen(function* () {
|
|
1703
|
+
yield* ensureAuth(false);
|
|
1704
|
+
const response = yield* withRetry(
|
|
1705
|
+
withRateLimit(
|
|
1706
|
+
Effect.tryPromise<AppBskyFeedGetFeedGenerator.Response>(() =>
|
|
1707
|
+
agent.app.bsky.feed.getFeedGenerator({ feed: uri })
|
|
1708
|
+
)
|
|
1709
|
+
)
|
|
1710
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch feed generator", "getFeedGenerator")));
|
|
1711
|
+
const view = yield* decodeFeedGeneratorView(response.data.view);
|
|
1712
|
+
return {
|
|
1713
|
+
view,
|
|
1714
|
+
isOnline: response.data.isOnline,
|
|
1715
|
+
isValid: response.data.isValid
|
|
1716
|
+
};
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
const getFeedGenerators = (uris: ReadonlyArray<string>) =>
|
|
1720
|
+
Effect.gen(function* () {
|
|
1721
|
+
yield* ensureAuth(false);
|
|
1722
|
+
if (uris.length === 0) {
|
|
1723
|
+
return { feeds: [] as ReadonlyArray<FeedGeneratorView> };
|
|
1724
|
+
}
|
|
1725
|
+
const response = yield* withRetry(
|
|
1726
|
+
withRateLimit(
|
|
1727
|
+
Effect.tryPromise<AppBskyFeedGetFeedGenerators.Response>(() =>
|
|
1728
|
+
agent.app.bsky.feed.getFeedGenerators({ feeds: [...uris] })
|
|
1729
|
+
)
|
|
1730
|
+
)
|
|
1731
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch feed generators", "getFeedGenerators")));
|
|
1732
|
+
const feeds = yield* Effect.forEach(
|
|
1733
|
+
response.data.feeds,
|
|
1734
|
+
decodeFeedGeneratorView,
|
|
1735
|
+
{ concurrency: "unbounded" }
|
|
1736
|
+
);
|
|
1737
|
+
return { feeds };
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
const getActorFeeds = (actor: string, opts?: ActorFeedsOptions) =>
|
|
1741
|
+
Effect.gen(function* () {
|
|
1742
|
+
yield* ensureAuth(false);
|
|
1743
|
+
const params = withCursor(
|
|
1744
|
+
{ actor, limit: opts?.limit ?? 50 },
|
|
1745
|
+
opts?.cursor
|
|
1746
|
+
);
|
|
1747
|
+
const response = yield* withRetry(
|
|
1748
|
+
withRateLimit(
|
|
1749
|
+
Effect.tryPromise<AppBskyFeedGetActorFeeds.Response>(() =>
|
|
1750
|
+
agent.app.bsky.feed.getActorFeeds(params)
|
|
1751
|
+
)
|
|
1752
|
+
)
|
|
1753
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch actor feeds", "getActorFeeds")));
|
|
1754
|
+
const feeds = yield* Effect.forEach(
|
|
1755
|
+
response.data.feeds,
|
|
1756
|
+
decodeFeedGeneratorView,
|
|
1757
|
+
{ concurrency: "unbounded" }
|
|
1758
|
+
);
|
|
1759
|
+
const cursor = response.data.cursor;
|
|
1760
|
+
return cursor ? { feeds, cursor } : { feeds };
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
const getLikes = (uri: string, opts?: EngagementOptions) =>
|
|
1764
|
+
Effect.gen(function* () {
|
|
1765
|
+
yield* ensureAuth(false);
|
|
1766
|
+
const params = withCursor(
|
|
1767
|
+
{ uri, limit: opts?.limit ?? 50, ...(opts?.cid ? { cid: opts.cid } : {}) },
|
|
1768
|
+
opts?.cursor
|
|
1769
|
+
);
|
|
1770
|
+
const response = yield* withRetry(
|
|
1771
|
+
withRateLimit(
|
|
1772
|
+
Effect.tryPromise<AppBskyFeedGetLikes.Response>(() =>
|
|
1773
|
+
agent.app.bsky.feed.getLikes(params)
|
|
1774
|
+
)
|
|
1775
|
+
)
|
|
1776
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch likes", "getLikes")));
|
|
1777
|
+
const likes = yield* Effect.forEach(
|
|
1778
|
+
response.data.likes,
|
|
1779
|
+
decodePostLike,
|
|
1780
|
+
{ concurrency: "unbounded" }
|
|
1781
|
+
);
|
|
1782
|
+
const cursor = response.data.cursor;
|
|
1783
|
+
return {
|
|
1784
|
+
uri: response.data.uri,
|
|
1785
|
+
...(typeof response.data.cid === "string" ? { cid: response.data.cid } : {}),
|
|
1786
|
+
likes,
|
|
1787
|
+
...(cursor ? { cursor } : {})
|
|
1788
|
+
};
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
const getRepostedBy = (uri: string, opts?: EngagementOptions) =>
|
|
1792
|
+
Effect.gen(function* () {
|
|
1793
|
+
yield* ensureAuth(false);
|
|
1794
|
+
const params = withCursor(
|
|
1795
|
+
{ uri, limit: opts?.limit ?? 50, ...(opts?.cid ? { cid: opts.cid } : {}) },
|
|
1796
|
+
opts?.cursor
|
|
1797
|
+
);
|
|
1798
|
+
const response = yield* withRetry(
|
|
1799
|
+
withRateLimit(
|
|
1800
|
+
Effect.tryPromise<AppBskyFeedGetRepostedBy.Response>(() =>
|
|
1801
|
+
agent.app.bsky.feed.getRepostedBy(params)
|
|
1802
|
+
)
|
|
1803
|
+
)
|
|
1804
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch reposts", "getRepostedBy")));
|
|
1805
|
+
const repostedBy = yield* Effect.forEach(
|
|
1806
|
+
response.data.repostedBy,
|
|
1807
|
+
decodeProfileView,
|
|
1808
|
+
{ concurrency: "unbounded" }
|
|
1809
|
+
);
|
|
1810
|
+
const cursor = response.data.cursor;
|
|
1811
|
+
return {
|
|
1812
|
+
uri: response.data.uri,
|
|
1813
|
+
...(typeof response.data.cid === "string" ? { cid: response.data.cid } : {}),
|
|
1814
|
+
repostedBy,
|
|
1815
|
+
...(cursor ? { cursor } : {})
|
|
1816
|
+
};
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
const getQuotes = (uri: string, opts?: EngagementOptions) =>
|
|
1820
|
+
Effect.gen(function* () {
|
|
1821
|
+
yield* ensureAuth(false);
|
|
1822
|
+
const params = withCursor(
|
|
1823
|
+
{ uri, limit: opts?.limit ?? 50, ...(opts?.cid ? { cid: opts.cid } : {}) },
|
|
1824
|
+
opts?.cursor
|
|
1825
|
+
);
|
|
1826
|
+
const response = yield* withRetry(
|
|
1827
|
+
withRateLimit(
|
|
1828
|
+
Effect.tryPromise<AppBskyFeedGetQuotes.Response>(() =>
|
|
1829
|
+
agent.app.bsky.feed.getQuotes(params)
|
|
1830
|
+
)
|
|
1831
|
+
)
|
|
1832
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch quotes", "getQuotes")));
|
|
1833
|
+
const posts = yield* Effect.forEach(
|
|
1834
|
+
response.data.posts,
|
|
1835
|
+
(post) => toRawPost(post),
|
|
1836
|
+
{ concurrency: "unbounded" }
|
|
1837
|
+
);
|
|
1838
|
+
const cursor = response.data.cursor;
|
|
1839
|
+
return {
|
|
1840
|
+
uri: response.data.uri,
|
|
1841
|
+
...(typeof response.data.cid === "string" ? { cid: response.data.cid } : {}),
|
|
1842
|
+
posts,
|
|
1843
|
+
...(cursor ? { cursor } : {})
|
|
1844
|
+
};
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
const resolveHandle = (handle: string) =>
|
|
1848
|
+
Effect.gen(function* () {
|
|
1849
|
+
yield* ensureAuth(false);
|
|
1850
|
+
const response = yield* withRetry(
|
|
1851
|
+
withRateLimit(
|
|
1852
|
+
Effect.tryPromise(() => agent.resolveHandle({ handle }))
|
|
1853
|
+
)
|
|
1854
|
+
).pipe(Effect.mapError(toBskyError("Failed to resolve handle", "resolveHandle")));
|
|
1855
|
+
return yield* decodeDid(response.data.did, "Invalid DID from resolveHandle");
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
const resolveIdentity = (identifier: string) =>
|
|
1859
|
+
Effect.gen(function* () {
|
|
1860
|
+
yield* ensureAuth(false);
|
|
1861
|
+
const response = yield* withRetry(
|
|
1862
|
+
withRateLimit(
|
|
1863
|
+
Effect.tryPromise<ComAtprotoIdentityResolveIdentity.Response>(() =>
|
|
1864
|
+
agent.com.atproto.identity.resolveIdentity({ identifier })
|
|
1865
|
+
)
|
|
1866
|
+
)
|
|
1867
|
+
).pipe(
|
|
1868
|
+
Effect.mapError(toBskyError("Failed to resolve identity", "resolveIdentity"))
|
|
1869
|
+
);
|
|
1870
|
+
return yield* decodeIdentityInfo(response.data);
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
const getProfiles = (actors: ReadonlyArray<string>) =>
|
|
1874
|
+
Effect.gen(function* () {
|
|
1875
|
+
const uniqueActors = Array.from(new Set(actors));
|
|
1876
|
+
if (uniqueActors.length === 0) {
|
|
1877
|
+
return [];
|
|
1878
|
+
}
|
|
1879
|
+
yield* ensureAuth(false);
|
|
1880
|
+
const batches = chunkArray(uniqueActors, 25);
|
|
1881
|
+
const results = yield* Effect.forEach(
|
|
1882
|
+
batches,
|
|
1883
|
+
(batch) =>
|
|
1884
|
+
withRetry(
|
|
1885
|
+
withRateLimit(
|
|
1886
|
+
Effect.tryPromise<AppBskyActorGetProfiles.Response>(() =>
|
|
1887
|
+
agent.app.bsky.actor.getProfiles({ actors: batch })
|
|
1888
|
+
)
|
|
1889
|
+
)
|
|
1890
|
+
).pipe(
|
|
1891
|
+
Effect.mapError(toBskyError("Failed to fetch profiles", "getProfiles")),
|
|
1892
|
+
Effect.flatMap((response) =>
|
|
1893
|
+
Effect.forEach(response.data.profiles, decodeProfileBasic, {
|
|
1894
|
+
concurrency: "unbounded"
|
|
1895
|
+
})
|
|
1896
|
+
)
|
|
1897
|
+
),
|
|
1898
|
+
{ concurrency: "unbounded" }
|
|
1899
|
+
);
|
|
1900
|
+
return results.flat();
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
const searchActors = (query: string, opts?: ActorSearchOptions) =>
|
|
1904
|
+
Effect.gen(function* () {
|
|
1905
|
+
yield* ensureAuth(false);
|
|
1906
|
+
if (opts?.typeahead) {
|
|
1907
|
+
const response = yield* withRetry(
|
|
1908
|
+
withRateLimit(
|
|
1909
|
+
Effect.tryPromise<AppBskyActorSearchActorsTypeahead.Response>(() =>
|
|
1910
|
+
agent.app.bsky.actor.searchActorsTypeahead({
|
|
1911
|
+
q: query,
|
|
1912
|
+
limit: opts.limit ?? 10
|
|
1913
|
+
})
|
|
1914
|
+
)
|
|
1915
|
+
)
|
|
1916
|
+
).pipe(Effect.mapError(toBskyError("Failed to search actors", "searchActorsTypeahead")));
|
|
1917
|
+
const actors = yield* Effect.forEach(
|
|
1918
|
+
response.data.actors,
|
|
1919
|
+
decodeProfileView,
|
|
1920
|
+
{ concurrency: "unbounded" }
|
|
1921
|
+
);
|
|
1922
|
+
return { actors };
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const params = withCursor(
|
|
1926
|
+
{ q: query, limit: opts?.limit ?? 25 },
|
|
1927
|
+
opts?.cursor
|
|
1928
|
+
);
|
|
1929
|
+
const response = yield* withRetry(
|
|
1930
|
+
withRateLimit(
|
|
1931
|
+
Effect.tryPromise<AppBskyActorSearchActors.Response>(() =>
|
|
1932
|
+
agent.app.bsky.actor.searchActors(params)
|
|
1933
|
+
)
|
|
1934
|
+
)
|
|
1935
|
+
).pipe(Effect.mapError(toBskyError("Failed to search actors", "searchActors")));
|
|
1936
|
+
const actors = yield* Effect.forEach(
|
|
1937
|
+
response.data.actors,
|
|
1938
|
+
decodeProfileView,
|
|
1939
|
+
{ concurrency: "unbounded" }
|
|
1940
|
+
);
|
|
1941
|
+
const cursor = response.data.cursor;
|
|
1942
|
+
return cursor ? { actors, cursor } : { actors };
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
const searchFeedGenerators = (query: string, opts?: FeedSearchOptions) =>
|
|
1946
|
+
Effect.gen(function* () {
|
|
1947
|
+
yield* ensureAuth(false);
|
|
1948
|
+
const params = withCursor(
|
|
1949
|
+
{ query, limit: opts?.limit ?? 25 },
|
|
1950
|
+
opts?.cursor
|
|
1951
|
+
);
|
|
1952
|
+
const response = yield* withRetry(
|
|
1953
|
+
withRateLimit(
|
|
1954
|
+
Effect.tryPromise<AppBskyUnspeccedGetPopularFeedGenerators.Response>(() =>
|
|
1955
|
+
agent.app.bsky.unspecced.getPopularFeedGenerators(params)
|
|
1956
|
+
)
|
|
1957
|
+
)
|
|
1958
|
+
).pipe(Effect.mapError(toBskyError("Failed to search feed generators", "searchFeedGenerators")));
|
|
1959
|
+
const feeds = yield* Effect.forEach(
|
|
1960
|
+
response.data.feeds,
|
|
1961
|
+
decodeFeedGeneratorView,
|
|
1962
|
+
{ concurrency: "unbounded" }
|
|
1963
|
+
);
|
|
1964
|
+
const cursor = response.data.cursor;
|
|
1965
|
+
return cursor ? { feeds, cursor } : { feeds };
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
const searchPosts = (query: string, opts?: NetworkSearchOptions) =>
|
|
1969
|
+
Effect.gen(function* () {
|
|
1970
|
+
yield* ensureAuth(false);
|
|
1971
|
+
const params = {
|
|
1972
|
+
q: query,
|
|
1973
|
+
...(opts?.sort ? { sort: opts.sort } : {}),
|
|
1974
|
+
...(opts?.since ? { since: opts.since } : {}),
|
|
1975
|
+
...(opts?.until ? { until: opts.until } : {}),
|
|
1976
|
+
...(opts?.mentions ? { mentions: opts.mentions } : {}),
|
|
1977
|
+
...(opts?.author ? { author: opts.author } : {}),
|
|
1978
|
+
...(opts?.lang ? { lang: opts.lang } : {}),
|
|
1979
|
+
...(opts?.domain ? { domain: opts.domain } : {}),
|
|
1980
|
+
...(opts?.url ? { url: opts.url } : {}),
|
|
1981
|
+
...(opts?.tags && opts.tags.length > 0 ? { tag: [...opts.tags] } : {}),
|
|
1982
|
+
limit: opts?.limit ?? 25,
|
|
1983
|
+
...(opts?.cursor ? { cursor: opts.cursor } : {})
|
|
1984
|
+
};
|
|
1985
|
+
const response = yield* withRetry(
|
|
1986
|
+
withRateLimit(
|
|
1987
|
+
Effect.tryPromise<AppBskyFeedSearchPosts.Response>(() =>
|
|
1988
|
+
agent.app.bsky.feed.searchPosts(params)
|
|
1989
|
+
)
|
|
1990
|
+
)
|
|
1991
|
+
).pipe(Effect.mapError(toBskyError("Failed to search posts", "searchPosts")));
|
|
1992
|
+
const posts = yield* Effect.forEach(
|
|
1993
|
+
response.data.posts,
|
|
1994
|
+
(post) => toRawPost(post),
|
|
1995
|
+
{ concurrency: "unbounded" }
|
|
1996
|
+
);
|
|
1997
|
+
const cursor = response.data.cursor;
|
|
1998
|
+
const hitsTotal = response.data.hitsTotal;
|
|
1999
|
+
return {
|
|
2000
|
+
posts,
|
|
2001
|
+
...(typeof cursor === "string" ? { cursor } : {}),
|
|
2002
|
+
...(typeof hitsTotal === "number" ? { hitsTotal } : {})
|
|
2003
|
+
};
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
const getNotifications = (opts?: NotificationsOptions) =>
|
|
2007
|
+
paginate(opts?.cursor, (cursor) =>
|
|
2008
|
+
Effect.gen(function* () {
|
|
2009
|
+
yield* ensureAuth(true);
|
|
2010
|
+
const params = withCursor({ limit: opts?.limit ?? 50 }, cursor);
|
|
2011
|
+
const response = yield* withRetry(
|
|
2012
|
+
withRateLimit(
|
|
2013
|
+
Effect.tryPromise<AppBskyNotificationListNotifications.Response>(() =>
|
|
2014
|
+
agent.app.bsky.notification.listNotifications(params)
|
|
2015
|
+
)
|
|
2016
|
+
)
|
|
2017
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch notifications", "listNotifications")));
|
|
2018
|
+
|
|
2019
|
+
const posts = yield* Effect.forEach(
|
|
2020
|
+
response.data.notifications,
|
|
2021
|
+
(notification) =>
|
|
2022
|
+
Effect.gen(function* () {
|
|
2023
|
+
if (!isPostRecord(notification.record)) {
|
|
2024
|
+
return Option.none<RawPost>();
|
|
2025
|
+
}
|
|
2026
|
+
const raw = {
|
|
2027
|
+
uri: notification.uri,
|
|
2028
|
+
cid: notification.cid,
|
|
2029
|
+
author: notification.author.handle,
|
|
2030
|
+
authorDid: notification.author.did,
|
|
2031
|
+
record: notification.record,
|
|
2032
|
+
indexedAt: notification.indexedAt,
|
|
2033
|
+
labels: notification.labels
|
|
2034
|
+
};
|
|
2035
|
+
const parsed = yield* Schema.decodeUnknown(RawPost)(raw).pipe(
|
|
2036
|
+
Effect.mapError(toBskyError("Invalid notification payload"))
|
|
2037
|
+
);
|
|
2038
|
+
return Option.some(parsed);
|
|
2039
|
+
})
|
|
2040
|
+
);
|
|
2041
|
+
|
|
2042
|
+
const filtered = posts.flatMap((item) =>
|
|
2043
|
+
Option.isSome(item) ? [item.value] : []
|
|
2044
|
+
);
|
|
2045
|
+
const nextCursor = response.data.cursor;
|
|
2046
|
+
const tagged = filtered.map((p) => new RawPost({ ...p, _pageCursor: nextCursor }));
|
|
2047
|
+
const hasNext =
|
|
2048
|
+
tagged.length > 0 &&
|
|
2049
|
+
typeof nextCursor === "string" &&
|
|
2050
|
+
nextCursor !== cursor;
|
|
2051
|
+
return [
|
|
2052
|
+
Chunk.fromIterable(tagged),
|
|
2053
|
+
hasNext ? Option.some(nextCursor) : Option.none()
|
|
2054
|
+
] as const;
|
|
2055
|
+
})
|
|
2056
|
+
);
|
|
2057
|
+
|
|
2058
|
+
const getTrendingTopics = Effect.gen(function* () {
|
|
2059
|
+
yield* ensureAuth(false);
|
|
2060
|
+
const response = yield* withRetry(
|
|
2061
|
+
withRateLimit(
|
|
2062
|
+
Effect.tryPromise<AppBskyUnspeccedGetTrendingTopics.Response>(() =>
|
|
2063
|
+
agent.app.bsky.unspecced.getTrendingTopics({
|
|
2064
|
+
limit: 25,
|
|
2065
|
+
...(agent.did ? { viewer: agent.did } : {})
|
|
2066
|
+
})
|
|
2067
|
+
)
|
|
2068
|
+
)
|
|
2069
|
+
).pipe(Effect.mapError(toBskyError("Failed to fetch trending topics", "getTrendingTopics")));
|
|
2070
|
+
|
|
2071
|
+
const topics = [
|
|
2072
|
+
...response.data.topics,
|
|
2073
|
+
...response.data.suggested
|
|
2074
|
+
]
|
|
2075
|
+
.map((topic) => topic.topic.trim().toLowerCase().replace(/^#/, ""))
|
|
2076
|
+
.filter((topic) => topic.length > 0);
|
|
2077
|
+
|
|
2078
|
+
return Array.from(new Set(topics));
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
return BskyClient.of({
|
|
2082
|
+
getTimeline,
|
|
2083
|
+
getNotifications,
|
|
2084
|
+
getFeed,
|
|
2085
|
+
getListFeed,
|
|
2086
|
+
getAuthorFeed,
|
|
2087
|
+
getPost,
|
|
2088
|
+
getPostThread,
|
|
2089
|
+
getFollowers,
|
|
2090
|
+
getFollows,
|
|
2091
|
+
getKnownFollowers,
|
|
2092
|
+
getRelationships,
|
|
2093
|
+
getList,
|
|
2094
|
+
getLists,
|
|
2095
|
+
getBlocks,
|
|
2096
|
+
getMutes,
|
|
2097
|
+
getFeedGenerator,
|
|
2098
|
+
getFeedGenerators,
|
|
2099
|
+
getActorFeeds,
|
|
2100
|
+
getLikes,
|
|
2101
|
+
getRepostedBy,
|
|
2102
|
+
getQuotes,
|
|
2103
|
+
resolveHandle,
|
|
2104
|
+
resolveIdentity,
|
|
2105
|
+
getProfiles,
|
|
2106
|
+
searchActors,
|
|
2107
|
+
searchFeedGenerators,
|
|
2108
|
+
searchPosts,
|
|
2109
|
+
getTrendingTopics: () => getTrendingTopics
|
|
2110
|
+
});
|
|
2111
|
+
})
|
|
2112
|
+
);
|
|
2113
|
+
}
|