+94
-4
api/scripts/generate_typescript.ts
+94
-4
api/scripts/generate_typescript.ts
···
19
19
interface LexiconDefinition {
20
20
type: string;
21
21
record?: LexiconRecord;
22
+
properties?: Record<string, LexiconProperty>;
22
23
}
23
24
24
25
interface Lexicon {
···
529
530
// Convert lexicon type to TypeScript type
530
531
function convertLexiconTypeToTypeScript(
531
532
def: any,
532
-
currentLexicon: string
533
+
currentLexicon: string,
534
+
propertyName?: string
533
535
): string {
534
536
const type = def.type;
535
537
switch (type) {
536
538
case "string":
539
+
// For knownValues, return the type alias name
540
+
if (def.knownValues && Array.isArray(def.knownValues) && def.knownValues.length > 0 && propertyName) {
541
+
// Reference the generated type alias with namespace
542
+
const namespace = nsidToNamespace(currentLexicon);
543
+
return `${namespace}${defNameToPascalCase(propertyName)}`;
544
+
}
537
545
return "string";
538
546
case "integer":
539
547
return "number";
···
736
744
737
745
if (defValue.properties) {
738
746
for (const [propName, propDef] of Object.entries(defValue.properties)) {
739
-
const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id);
747
+
const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id, propName);
740
748
const required =
741
749
defValue.required && defValue.required.includes(propName);
742
750
···
775
783
776
784
if (recordDef.properties) {
777
785
for (const [propName, propDef] of Object.entries(recordDef.properties)) {
778
-
const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id);
786
+
const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id, propName);
779
787
const required = isPropertyRequired(recordDef, propName);
780
788
781
789
properties.push({
···
866
874
});
867
875
}
868
876
877
+
// Generate type aliases for string fields with knownValues
878
+
function generateKnownValuesTypes(): void {
879
+
for (const lexicon of lexicons) {
880
+
if (lexicon.definitions && typeof lexicon.definitions === "object") {
881
+
for (const [defKey, defValue] of Object.entries(lexicon.definitions)) {
882
+
if (defValue.type === "record" && defValue.record?.properties) {
883
+
for (const [propName, propDef] of Object.entries(defValue.record.properties)) {
884
+
const prop = propDef as any;
885
+
if (prop.type === "string" && prop.knownValues && Array.isArray(prop.knownValues) && prop.knownValues.length > 0) {
886
+
// Generate a type alias for this property, namespaced by lexicon
887
+
const namespace = nsidToNamespace(lexicon.id);
888
+
const pascalPropName = defNameToPascalCase(propName);
889
+
const typeName = `${namespace}${pascalPropName}`;
890
+
891
+
const knownValueTypes = prop.knownValues.map((value: string) => `'${value}'`).join('\n | ');
892
+
const typeDefinition = `${knownValueTypes}\n | (string & {})`;
893
+
894
+
sourceFile.addTypeAlias({
895
+
name: typeName,
896
+
isExported: true,
897
+
type: typeDefinition,
898
+
leadingTrivia: "\n",
899
+
});
900
+
}
901
+
}
902
+
} else if (defValue.type === "object" && defValue.properties) {
903
+
for (const [propName, propDef] of Object.entries(defValue.properties)) {
904
+
const prop = propDef as any;
905
+
if (prop.type === "string" && prop.knownValues && Array.isArray(prop.knownValues) && prop.knownValues.length > 0) {
906
+
// Generate a type alias for this property, namespaced by lexicon
907
+
const namespace = nsidToNamespace(lexicon.id);
908
+
const pascalPropName = defNameToPascalCase(propName);
909
+
const typeName = `${namespace}${pascalPropName}`;
910
+
911
+
const knownValueTypes = prop.knownValues.map((value: string) => `'${value}'`).join('\n | ');
912
+
const typeDefinition = `${knownValueTypes}\n | (string & {})`;
913
+
914
+
sourceFile.addTypeAlias({
915
+
name: typeName,
916
+
isExported: true,
917
+
type: typeDefinition,
918
+
leadingTrivia: "\n",
919
+
});
920
+
}
921
+
}
922
+
} else if (defValue.type === "string") {
923
+
// Handle standalone string definitions with knownValues (like labelValue)
924
+
const stringDef = defValue as any;
925
+
if (stringDef.knownValues && Array.isArray(stringDef.knownValues) && stringDef.knownValues.length > 0) {
926
+
// Generate a type alias for this definition, namespaced by lexicon
927
+
const namespace = nsidToNamespace(lexicon.id);
928
+
const typeName = `${namespace}${defNameToPascalCase(defKey)}`;
929
+
930
+
const knownValueTypes = stringDef.knownValues.map((value: string) => `'${value}'`).join('\n | ');
931
+
const typeDefinition = `${knownValueTypes}\n | (string & {})`;
932
+
933
+
sourceFile.addTypeAlias({
934
+
name: typeName,
935
+
isExported: true,
936
+
type: typeDefinition,
937
+
leadingTrivia: "\n",
938
+
});
939
+
}
940
+
}
941
+
}
942
+
}
943
+
}
944
+
}
945
+
869
946
// Add lexicon-specific interfaces and types
870
947
function addLexiconInterfaces(): void {
871
948
// First pass: Generate all individual definition interfaces/types
···
932
1009
isReadonly: true,
933
1010
});
934
1011
break;
1012
+
case "string":
1013
+
// Check if this is a string type with knownValues
1014
+
const stringDef = defValue as any;
1015
+
if (stringDef.knownValues && Array.isArray(stringDef.knownValues) && stringDef.knownValues.length > 0) {
1016
+
// This generates a type alias, reference it in the namespace with full name
1017
+
namespaceProperties.push({
1018
+
name: defName,
1019
+
type: `${namespace}${defNameToPascalCase(defKey)}`,
1020
+
isReadonly: true,
1021
+
});
1022
+
}
1023
+
break;
935
1024
case "union":
936
1025
case "array":
937
1026
case "token":
···
1144
1233
{
1145
1234
name: "client",
1146
1235
type: "SlicesClient",
1147
-
scope: "private",
1236
+
scope: "private" as any,
1148
1237
isReadonly: true,
1149
1238
},
1150
1239
]),
···
1441
1530
1442
1531
// Generate the TypeScript
1443
1532
addBaseInterfaces();
1533
+
generateKnownValuesTypes();
1444
1534
addLexiconInterfaces();
1445
1535
addClientClass();
1446
1536
+2
-6
api/src/handler_xrpc_dynamic.rs
+2
-6
api/src/handler_xrpc_dynamic.rs
···
575
575
576
576
match LexiconValidator::for_slice(&state.database, validation_slice_uri).await {
577
577
Ok(validator) => {
578
-
// Debug: Get lexicons from the system slice to see what's there
579
-
if collection == "network.slices.lexicon" {}
580
578
581
579
if let Err(e) = validator.validate_record(&collection, &record_data) {
582
580
return Err((
···
588
586
));
589
587
}
590
588
}
591
-
Err(e) => {
589
+
Err(_e) => {
592
590
// If no lexicon found, continue without validation (backwards compatibility)
593
-
eprintln!("Could not load lexicon validator: {:?}", e);
594
591
}
595
592
}
596
593
···
689
686
));
690
687
}
691
688
}
692
-
Err(e) => {
689
+
Err(_e) => {
693
690
// If no lexicon found, continue without validation (backwards compatibility)
694
-
eprintln!("Could not load lexicon validator: {:?}", e);
695
691
}
696
692
}
697
693
+1
-1
docker-compose.yml
+1
-1
docker-compose.yml
···
35
35
HTTP_PORT: "8081"
36
36
DATABASE_URL: "sqlite:///data/aip.db"
37
37
HTTP_CLIENT_TIMEOUT: "30"
38
-
OAUTH_SUPPORTED_SCOPES: "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waiting"
38
+
OAUTH_SUPPORTED_SCOPES: "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waitlist.request"
39
39
ENABLE_CLIENT_API: "true"
40
40
ADMIN_DIDS: "did:plc:bcgltzqazw5tb6k2g3ttenbj"
41
41
DPOP_NONCE_SEED: "local-dev-nonce-seed"
+5
-3
docs/sdk-usage.md
+5
-3
docs/sdk-usage.md
···
118
118
where: {
119
119
releaseDate: {
120
120
gte: "1990-01-01",
121
-
lte: "1999-12-31"
121
+
lte: "1999-12-31",
122
122
},
123
123
},
124
124
});
···
368
368
// Filter by exact handle
369
369
const exactHandle = await client.network.slices.slice.getActors({
370
370
where: {
371
-
handle: { eq: "alice.bsky.social" },
371
+
handle: { eq: "user.bsky.social" },
372
372
},
373
373
});
374
374
···
492
492
// Limit to specific collections
493
493
const specificSearch = await client.network.slices.slice.getSliceRecords({
494
494
where: {
495
-
collection: { in: ["com.recordcollector.album", "com.recordcollector.review"] },
495
+
collection: {
496
+
in: ["com.recordcollector.album", "com.recordcollector.review"],
497
+
},
496
498
json: { contains: "grunge" },
497
499
},
498
500
});
+1
-1
frontend/deno.json
+1
-1
frontend/deno.json
···
8
8
"jsxImportSource": "preact"
9
9
},
10
10
"imports": {
11
-
"@slices/client": "jsr:@slices/client@^0.1.0-alpha.2",
11
+
"@slices/client": "jsr:@slices/client@^0.1.0-alpha.3",
12
12
"@slices/oauth": "jsr:@slices/oauth@^0.4.1",
13
13
"@slices/session": "jsr:@slices/session@^0.2.1",
14
14
"@std/assert": "jsr:@std/assert@^1.0.14",
+4
-7
frontend/deno.lock
+4
-7
frontend/deno.lock
···
2
2
"version": "5",
3
3
"specifiers": {
4
4
"jsr:@shikijs/shiki@*": "3.7.0",
5
-
"jsr:@slices/client@~0.1.0-alpha.2": "0.1.0-alpha.2",
5
+
"jsr:@slices/client@~0.1.0-alpha.3": "0.1.0-alpha.3",
6
6
"jsr:@slices/oauth@~0.4.1": "0.4.1",
7
7
"jsr:@slices/session@~0.2.1": "0.2.1",
8
8
"jsr:@std/assert@^1.0.14": "1.0.14",
···
43
43
"npm:shiki"
44
44
]
45
45
},
46
-
"@slices/client@0.1.0-alpha.2": {
47
-
"integrity": "d3c591e89ab5b7ed7988faf9428bb7b3539484c6b90005a7c66f2188cc60fe19",
48
-
"dependencies": [
49
-
"jsr:@slices/oauth"
50
-
]
46
+
"@slices/client@0.1.0-alpha.3": {
47
+
"integrity": "c18d6ad2dbe1043bbeb7da7c5a11724fa0fa388c3e6e96089bb033f518c4b23c"
51
48
},
52
49
"@slices/oauth@0.4.1": {
53
50
"integrity": "15f20df2ba81e9d1764291c8b4f6e3eb38cfc953750eeb3815872b7e22475492"
···
672
669
},
673
670
"workspace": {
674
671
"dependencies": [
675
-
"jsr:@slices/client@~0.1.0-alpha.2",
672
+
"jsr:@slices/client@~0.1.0-alpha.3",
676
673
"jsr:@slices/oauth@~0.4.1",
677
674
"jsr:@slices/session@~0.2.1",
678
675
"jsr:@std/assert@^1.0.14",
+2
-2
frontend/scripts/register-oauth-client.sh
+2
-2
frontend/scripts/register-oauth-client.sh
···
45
45
{
46
46
"client_name": "$CLIENT_NAME",
47
47
"redirect_uris": ["$REDIRECT_URI"],
48
-
"scope": "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waiting",
48
+
"scope": "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waitlist.request",
49
49
"grant_types": ["authorization_code", "refresh_token"],
50
50
"response_types": ["code"],
51
51
"token_endpoint_auth_method": "client_secret_basic"
···
106
106
echo " - Client ID: $CLIENT_ID"
107
107
echo " - Client Name: $CLIENT_NAME"
108
108
echo " - Redirect URI: $REDIRECT_URI"
109
-
echo " - Scopes: openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waiting"
109
+
echo " - Scopes: openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waitlist.request"
110
110
echo " - Config saved to: $CONFIG_FILE"
111
111
echo
112
112
echo "🔧 Environment variables saved to $CONFIG_FILE:"
+1484
-99
frontend/src/client.ts
+1484
-99
frontend/src/client.ts
···
1
1
// Generated TypeScript client for AT Protocol records
2
-
// Generated at: 2025-09-16 21:02:30 UTC
3
-
// Lexicons: 9
2
+
// Generated at: 2025-09-17 17:42:43 UTC
3
+
// Lexicons: 25
4
4
5
5
/**
6
6
* @example Usage
···
12
12
* 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z'
13
13
* );
14
14
*
15
-
* // Get records from the app.bsky.actor.profile collection
16
-
* const records = await client.app.bsky.actor.profile.getRecords();
15
+
* // Get records from the app.bsky.graph.follow collection
16
+
* const records = await client.app.bsky.graph.follow.getRecords();
17
17
*
18
18
* // Get a specific record
19
-
* const record = await client.app.bsky.actor.profile.getRecord({
20
-
* uri: 'at://did:plc:example/app.bsky.actor.profile/3abc123'
19
+
* const record = await client.app.bsky.graph.follow.getRecord({
20
+
* uri: 'at://did:plc:example/app.bsky.graph.follow/3abc123'
21
21
* });
22
22
*
23
23
* // Get records with filtering and search
24
-
* const filteredRecords = await client.app.bsky.actor.profile.getRecords({
24
+
* const filteredRecords = await client.app.bsky.graph.follow.getRecords({
25
25
* where: {
26
26
* text: { contains: "example search term" }
27
27
* }
28
28
* });
29
29
*
30
30
* // Use slice-level methods for cross-collection queries with type safety
31
-
* const sliceRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile>({
31
+
* const sliceRecords = await client.network.slices.slice.getSliceRecords<AppBskyGraphFollow>({
32
32
* where: {
33
-
* collection: { eq: 'app.bsky.actor.profile' }
33
+
* collection: { eq: 'app.bsky.graph.follow' }
34
34
* }
35
35
* });
36
36
*
37
37
* // Search across multiple collections using union types
38
-
* const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile | AppBskyActorProfile>({
38
+
* const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<AppBskyGraphFollow | AppBskyActorProfile>({
39
39
* where: {
40
-
* collection: { in: ['app.bsky.actor.profile', 'app.bsky.actor.profile'] },
40
+
* collection: { in: ['app.bsky.graph.follow', 'app.bsky.actor.profile'] },
41
41
* text: { contains: 'example search term' },
42
42
* did: { in: ['did:plc:user1', 'did:plc:user2'] }
43
43
* },
···
263
263
message: string;
264
264
}
265
265
266
+
export type AppBskyGraphDefsListPurpose =
267
+
| "app.bsky.graph.defs#modlist"
268
+
| "app.bsky.graph.defs#curatelist"
269
+
| "app.bsky.graph.defs#referencelist"
270
+
| (string & {});
271
+
272
+
export type AppBskyFeedDefsEvent =
273
+
| "app.bsky.feed.defs#requestLess"
274
+
| "app.bsky.feed.defs#requestMore"
275
+
| "app.bsky.feed.defs#clickthroughItem"
276
+
| "app.bsky.feed.defs#clickthroughAuthor"
277
+
| "app.bsky.feed.defs#clickthroughReposter"
278
+
| "app.bsky.feed.defs#clickthroughEmbed"
279
+
| "app.bsky.feed.defs#interactionSeen"
280
+
| "app.bsky.feed.defs#interactionLike"
281
+
| "app.bsky.feed.defs#interactionRepost"
282
+
| "app.bsky.feed.defs#interactionReply"
283
+
| "app.bsky.feed.defs#interactionQuote"
284
+
| "app.bsky.feed.defs#interactionShare"
285
+
| (string & {});
286
+
287
+
export type AppBskyFeedDefsContentMode =
288
+
| "app.bsky.feed.defs#contentModeUnspecified"
289
+
| "app.bsky.feed.defs#contentModeVideo"
290
+
| (string & {});
291
+
292
+
export type AppBskyActorDefsActorTarget =
293
+
| "all"
294
+
| "exclude-following"
295
+
| (string & {});
296
+
297
+
export type AppBskyActorDefsType = "feed" | "list" | "timeline" | (string & {});
298
+
299
+
export type AppBskyActorDefsSort =
300
+
| "oldest"
301
+
| "newest"
302
+
| "most-likes"
303
+
| "random"
304
+
| "hotness"
305
+
| (string & {});
306
+
307
+
export type AppBskyActorDefsMutedWordTarget = "content" | "tag" | (string & {});
308
+
309
+
export type AppBskyActorDefsVisibility =
310
+
| "ignore"
311
+
| "show"
312
+
| "warn"
313
+
| "hide"
314
+
| (string & {});
315
+
316
+
export type AppBskyActorDefsAllowIncoming =
317
+
| "all"
318
+
| "none"
319
+
| "following"
320
+
| (string & {});
321
+
322
+
export type ComAtprotoLabelDefsLabelValue =
323
+
| "!hide"
324
+
| "!no-promote"
325
+
| "!warn"
326
+
| "!no-unauthenticated"
327
+
| "dmca-violation"
328
+
| "doxxing"
329
+
| "porn"
330
+
| "sexual"
331
+
| "nudity"
332
+
| "nsfl"
333
+
| "gore"
334
+
| (string & {});
335
+
336
+
export type ComAtprotoLabelDefsBlurs =
337
+
| "content"
338
+
| "media"
339
+
| "none"
340
+
| (string & {});
341
+
342
+
export type ComAtprotoLabelDefsSeverity =
343
+
| "inform"
344
+
| "alert"
345
+
| "none"
346
+
| (string & {});
347
+
348
+
export type ComAtprotoLabelDefsDefaultSetting =
349
+
| "ignore"
350
+
| "warn"
351
+
| "hide"
352
+
| (string & {});
353
+
354
+
export interface AppBskyEmbedDefsAspectRatio {
355
+
width: number;
356
+
height: number;
357
+
}
358
+
359
+
export interface AppBskyEmbedRecordMain {
360
+
record: ComAtprotoRepoStrongRef;
361
+
}
362
+
363
+
export interface AppBskyEmbedRecordView {
364
+
record:
365
+
| AppBskyEmbedRecord["ViewRecord"]
366
+
| AppBskyEmbedRecord["ViewNotFound"]
367
+
| AppBskyEmbedRecord["ViewBlocked"]
368
+
| AppBskyEmbedRecord["ViewDetached"]
369
+
| AppBskyFeedDefs["GeneratorView"]
370
+
| AppBskyGraphDefs["ListView"]
371
+
| AppBskyLabelerDefs["LabelerView"]
372
+
| AppBskyGraphDefs["StarterPackViewBasic"]
373
+
| { $type: string; [key: string]: unknown };
374
+
}
375
+
376
+
export interface AppBskyEmbedRecordViewRecord {
377
+
cid: string;
378
+
uri: string;
379
+
/** The record data itself. */
380
+
value: unknown;
381
+
author: AppBskyActorDefs["ProfileViewBasic"];
382
+
embeds?:
383
+
| AppBskyEmbedImages["View"]
384
+
| AppBskyEmbedVideo["View"]
385
+
| AppBskyEmbedExternal["View"]
386
+
| AppBskyEmbedRecord["View"]
387
+
| AppBskyEmbedRecordWithMedia["View"]
388
+
| { $type: string; [key: string]: unknown }[];
389
+
labels?: ComAtprotoLabelDefs["Label"][];
390
+
indexedAt: string;
391
+
likeCount?: number;
392
+
quoteCount?: number;
393
+
replyCount?: number;
394
+
repostCount?: number;
395
+
}
396
+
397
+
export interface AppBskyEmbedRecordViewBlocked {
398
+
uri: string;
399
+
author: AppBskyFeedDefs["BlockedAuthor"];
400
+
blocked: boolean;
401
+
}
402
+
403
+
export interface AppBskyEmbedRecordViewDetached {
404
+
uri: string;
405
+
detached: boolean;
406
+
}
407
+
408
+
export interface AppBskyEmbedRecordViewNotFound {
409
+
uri: string;
410
+
notFound: boolean;
411
+
}
412
+
413
+
export interface AppBskyEmbedImagesMain {
414
+
images: AppBskyEmbedImages["Image"][];
415
+
}
416
+
417
+
export interface AppBskyEmbedImagesView {
418
+
images: AppBskyEmbedImages["ViewImage"][];
419
+
}
420
+
421
+
export interface AppBskyEmbedImagesImage {
422
+
/** Alt text description of the image, for accessibility. */
423
+
alt: string;
424
+
image: BlobRef;
425
+
aspectRatio?: AppBskyEmbedDefs["AspectRatio"];
426
+
}
427
+
428
+
export interface AppBskyEmbedImagesViewImage {
429
+
/** Alt text description of the image, for accessibility. */
430
+
alt: string;
431
+
/** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */
432
+
thumb: string;
433
+
/** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */
434
+
fullsize: string;
435
+
aspectRatio?: AppBskyEmbedDefs["AspectRatio"];
436
+
}
437
+
438
+
export interface AppBskyEmbedRecordWithMediaMain {
439
+
media:
440
+
| AppBskyEmbedImages["Main"]
441
+
| AppBskyEmbedVideo["Main"]
442
+
| AppBskyEmbedExternal["Main"]
443
+
| { $type: string; [key: string]: unknown };
444
+
record: AppBskyEmbedRecord["Main"];
445
+
}
446
+
447
+
export interface AppBskyEmbedRecordWithMediaView {
448
+
media:
449
+
| AppBskyEmbedImages["View"]
450
+
| AppBskyEmbedVideo["View"]
451
+
| AppBskyEmbedExternal["View"]
452
+
| { $type: string; [key: string]: unknown };
453
+
record: AppBskyEmbedRecord["View"];
454
+
}
455
+
456
+
export interface AppBskyEmbedVideoMain {
457
+
/** Alt text description of the video, for accessibility. */
458
+
alt?: string;
459
+
video: BlobRef;
460
+
captions?: AppBskyEmbedVideo["Caption"][];
461
+
aspectRatio?: AppBskyEmbedDefs["AspectRatio"];
462
+
}
463
+
464
+
export interface AppBskyEmbedVideoView {
465
+
alt?: string;
466
+
cid: string;
467
+
playlist: string;
468
+
thumbnail?: string;
469
+
aspectRatio?: AppBskyEmbedDefs["AspectRatio"];
470
+
}
471
+
472
+
export interface AppBskyEmbedVideoCaption {
473
+
file: BlobRef;
474
+
lang: string;
475
+
}
476
+
477
+
export interface AppBskyEmbedExternalMain {
478
+
external: AppBskyEmbedExternal["External"];
479
+
}
480
+
481
+
export interface AppBskyEmbedExternalView {
482
+
external: AppBskyEmbedExternal["ViewExternal"];
483
+
}
484
+
485
+
export interface AppBskyEmbedExternalExternal {
486
+
uri: string;
487
+
thumb?: BlobRef;
488
+
title: string;
489
+
description: string;
490
+
}
491
+
492
+
export interface AppBskyEmbedExternalViewExternal {
493
+
uri: string;
494
+
thumb?: string;
495
+
title: string;
496
+
description: string;
497
+
}
498
+
499
+
export interface AppBskyGraphFollow {
500
+
subject: string;
501
+
createdAt: string;
502
+
}
503
+
504
+
export type AppBskyGraphFollowSortFields = "subject" | "createdAt";
505
+
export type AppBskyGraphDefsModlist = "app.bsky.graph.defs#modlist";
506
+
507
+
export interface AppBskyGraphDefsListView {
508
+
cid: string;
509
+
uri: string;
510
+
name: string;
511
+
avatar?: string;
512
+
labels?: ComAtprotoLabelDefs["Label"][];
513
+
viewer?: AppBskyGraphDefs["ListViewerState"];
514
+
creator: AppBskyActorDefs["ProfileView"];
515
+
purpose: AppBskyGraphDefs["ListPurpose"];
516
+
indexedAt: string;
517
+
description?: string;
518
+
listItemCount?: number;
519
+
descriptionFacets?: AppBskyRichtextFacet["Main"][];
520
+
}
521
+
522
+
export type AppBskyGraphDefsCuratelist = "app.bsky.graph.defs#curatelist";
523
+
524
+
export interface AppBskyGraphDefsListItemView {
525
+
uri: string;
526
+
subject: AppBskyActorDefs["ProfileView"];
527
+
}
528
+
529
+
export interface AppBskyGraphDefsRelationship {
530
+
did: string;
531
+
/** if the actor follows this DID, this is the AT-URI of the follow record */
532
+
following?: string;
533
+
/** if the actor is followed by this DID, contains the AT-URI of the follow record */
534
+
followedBy?: string;
535
+
}
536
+
537
+
export interface AppBskyGraphDefsListViewBasic {
538
+
cid: string;
539
+
uri: string;
540
+
name: string;
541
+
avatar?: string;
542
+
labels?: ComAtprotoLabelDefs["Label"][];
543
+
viewer?: AppBskyGraphDefs["ListViewerState"];
544
+
purpose: AppBskyGraphDefs["ListPurpose"];
545
+
indexedAt?: string;
546
+
listItemCount?: number;
547
+
}
548
+
549
+
export interface AppBskyGraphDefsNotFoundActor {
550
+
actor: string;
551
+
notFound: boolean;
552
+
}
553
+
554
+
export type AppBskyGraphDefsReferencelist = "app.bsky.graph.defs#referencelist";
555
+
556
+
export interface AppBskyGraphDefsListViewerState {
557
+
muted?: boolean;
558
+
blocked?: string;
559
+
}
560
+
561
+
export interface AppBskyGraphDefsStarterPackView {
562
+
cid: string;
563
+
uri: string;
564
+
list?: AppBskyGraphDefs["ListViewBasic"];
565
+
feeds?: AppBskyFeedDefs["GeneratorView"][];
566
+
labels?: ComAtprotoLabelDefs["Label"][];
567
+
record: unknown;
568
+
creator: AppBskyActorDefs["ProfileViewBasic"];
569
+
indexedAt: string;
570
+
joinedWeekCount?: number;
571
+
listItemsSample?: AppBskyGraphDefs["ListItemView"][];
572
+
joinedAllTimeCount?: number;
573
+
}
574
+
575
+
export interface AppBskyGraphDefsStarterPackViewBasic {
576
+
cid: string;
577
+
uri: string;
578
+
labels?: ComAtprotoLabelDefs["Label"][];
579
+
record: unknown;
580
+
creator: AppBskyActorDefs["ProfileViewBasic"];
581
+
indexedAt: string;
582
+
listItemCount?: number;
583
+
joinedWeekCount?: number;
584
+
joinedAllTimeCount?: number;
585
+
}
586
+
587
+
export interface AppBskyFeedDefsPostView {
588
+
cid: string;
589
+
uri: string;
590
+
embed?:
591
+
| AppBskyEmbedImages["View"]
592
+
| AppBskyEmbedVideo["View"]
593
+
| AppBskyEmbedExternal["View"]
594
+
| AppBskyEmbedRecord["View"]
595
+
| AppBskyEmbedRecordWithMedia["View"]
596
+
| { $type: string; [key: string]: unknown };
597
+
author: AppBskyActorDefs["ProfileViewBasic"];
598
+
labels?: ComAtprotoLabelDefs["Label"][];
599
+
record: unknown;
600
+
viewer?: AppBskyFeedDefs["ViewerState"];
601
+
indexedAt: string;
602
+
likeCount?: number;
603
+
quoteCount?: number;
604
+
replyCount?: number;
605
+
threadgate?: AppBskyFeedDefs["ThreadgateView"];
606
+
repostCount?: number;
607
+
}
608
+
609
+
export interface AppBskyFeedDefsReplyRef {
610
+
root:
611
+
| AppBskyFeedDefs["PostView"]
612
+
| AppBskyFeedDefs["NotFoundPost"]
613
+
| AppBskyFeedDefs["BlockedPost"]
614
+
| { $type: string; [key: string]: unknown };
615
+
parent:
616
+
| AppBskyFeedDefs["PostView"]
617
+
| AppBskyFeedDefs["NotFoundPost"]
618
+
| AppBskyFeedDefs["BlockedPost"]
619
+
| { $type: string; [key: string]: unknown };
620
+
/** When parent is a reply to another post, this is the author of that post. */
621
+
grandparentAuthor?: AppBskyActorDefs["ProfileViewBasic"];
622
+
}
623
+
624
+
export interface AppBskyFeedDefsReasonPin {}
625
+
626
+
export interface AppBskyFeedDefsBlockedPost {
627
+
uri: string;
628
+
author: AppBskyFeedDefs["BlockedAuthor"];
629
+
blocked: boolean;
630
+
}
631
+
632
+
export interface AppBskyFeedDefsInteraction {
633
+
item?: string;
634
+
event?: AppBskyFeedDefsEvent;
635
+
/** Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton. */
636
+
feedContext?: string;
637
+
}
638
+
639
+
export type AppBskyFeedDefsRequestLess = "app.bsky.feed.defs#requestLess";
640
+
export type AppBskyFeedDefsRequestMore = "app.bsky.feed.defs#requestMore";
641
+
642
+
export interface AppBskyFeedDefsViewerState {
643
+
like?: string;
644
+
pinned?: boolean;
645
+
repost?: string;
646
+
threadMuted?: boolean;
647
+
replyDisabled?: boolean;
648
+
embeddingDisabled?: boolean;
649
+
}
650
+
651
+
export interface AppBskyFeedDefsFeedViewPost {
652
+
post: AppBskyFeedDefs["PostView"];
653
+
reply?: AppBskyFeedDefs["ReplyRef"];
654
+
reason?:
655
+
| AppBskyFeedDefs["ReasonRepost"]
656
+
| AppBskyFeedDefs["ReasonPin"]
657
+
| {
658
+
$type: string;
659
+
[key: string]: unknown;
660
+
};
661
+
/** Context provided by feed generator that may be passed back alongside interactions. */
662
+
feedContext?: string;
663
+
}
664
+
665
+
export interface AppBskyFeedDefsNotFoundPost {
666
+
uri: string;
667
+
notFound: boolean;
668
+
}
669
+
670
+
export interface AppBskyFeedDefsReasonRepost {
671
+
by: AppBskyActorDefs["ProfileViewBasic"];
672
+
indexedAt: string;
673
+
}
674
+
675
+
export interface AppBskyFeedDefsBlockedAuthor {
676
+
did: string;
677
+
viewer?: AppBskyActorDefs["ViewerState"];
678
+
}
679
+
680
+
export interface AppBskyFeedDefsGeneratorView {
681
+
cid: string;
682
+
did: string;
683
+
uri: string;
684
+
avatar?: string;
685
+
labels?: ComAtprotoLabelDefs["Label"][];
686
+
viewer?: AppBskyFeedDefs["GeneratorViewerState"];
687
+
creator: AppBskyActorDefs["ProfileView"];
688
+
indexedAt: string;
689
+
likeCount?: number;
690
+
contentMode?: AppBskyFeedDefsContentMode;
691
+
description?: string;
692
+
displayName: string;
693
+
descriptionFacets?: AppBskyRichtextFacet["Main"][];
694
+
acceptsInteractions?: boolean;
695
+
}
696
+
697
+
export interface AppBskyFeedDefsThreadContext {
698
+
rootAuthorLike?: string;
699
+
}
700
+
701
+
export interface AppBskyFeedDefsThreadViewPost {
702
+
post: AppBskyFeedDefs["PostView"];
703
+
parent?:
704
+
| AppBskyFeedDefs["ThreadViewPost"]
705
+
| AppBskyFeedDefs["NotFoundPost"]
706
+
| AppBskyFeedDefs["BlockedPost"]
707
+
| { $type: string; [key: string]: unknown };
708
+
replies?:
709
+
| AppBskyFeedDefs["ThreadViewPost"]
710
+
| AppBskyFeedDefs["NotFoundPost"]
711
+
| AppBskyFeedDefs["BlockedPost"]
712
+
| { $type: string; [key: string]: unknown }[];
713
+
threadContext?: AppBskyFeedDefs["ThreadContext"];
714
+
}
715
+
716
+
export interface AppBskyFeedDefsThreadgateView {
717
+
cid?: string;
718
+
uri?: string;
719
+
lists?: AppBskyGraphDefs["ListViewBasic"][];
720
+
record?: unknown;
721
+
}
722
+
723
+
export type AppBskyFeedDefsInteractionLike =
724
+
"app.bsky.feed.defs#interactionLike";
725
+
export type AppBskyFeedDefsInteractionSeen =
726
+
"app.bsky.feed.defs#interactionSeen";
727
+
export type AppBskyFeedDefsClickthroughItem =
728
+
"app.bsky.feed.defs#clickthroughItem";
729
+
export type AppBskyFeedDefsContentModeVideo =
730
+
"app.bsky.feed.defs#contentModeVideo";
731
+
export type AppBskyFeedDefsInteractionQuote =
732
+
"app.bsky.feed.defs#interactionQuote";
733
+
export type AppBskyFeedDefsInteractionReply =
734
+
"app.bsky.feed.defs#interactionReply";
735
+
export type AppBskyFeedDefsInteractionShare =
736
+
"app.bsky.feed.defs#interactionShare";
737
+
738
+
export interface AppBskyFeedDefsSkeletonFeedPost {
739
+
post: string;
740
+
reason?:
741
+
| AppBskyFeedDefs["SkeletonReasonRepost"]
742
+
| AppBskyFeedDefs["SkeletonReasonPin"]
743
+
| { $type: string; [key: string]: unknown };
744
+
/** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */
745
+
feedContext?: string;
746
+
}
747
+
748
+
export type AppBskyFeedDefsClickthroughEmbed =
749
+
"app.bsky.feed.defs#clickthroughEmbed";
750
+
export type AppBskyFeedDefsInteractionRepost =
751
+
"app.bsky.feed.defs#interactionRepost";
752
+
753
+
export interface AppBskyFeedDefsSkeletonReasonPin {}
754
+
755
+
export type AppBskyFeedDefsClickthroughAuthor =
756
+
"app.bsky.feed.defs#clickthroughAuthor";
757
+
export type AppBskyFeedDefsClickthroughReposter =
758
+
"app.bsky.feed.defs#clickthroughReposter";
759
+
760
+
export interface AppBskyFeedDefsGeneratorViewerState {
761
+
like?: string;
762
+
}
763
+
764
+
export interface AppBskyFeedDefsSkeletonReasonRepost {
765
+
repost: string;
766
+
}
767
+
768
+
export type AppBskyFeedDefsContentModeUnspecified =
769
+
"app.bsky.feed.defs#contentModeUnspecified";
770
+
771
+
export interface AppBskyFeedPostgate {
772
+
/** Reference (AT-URI) to the post record. */
773
+
post: string;
774
+
createdAt: string;
775
+
/** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */
776
+
embeddingRules?:
777
+
| AppBskyFeedPostgate["DisableRule"]
778
+
| {
779
+
$type: string;
780
+
[key: string]: unknown;
781
+
}[];
782
+
/** List of AT-URIs embedding this post that the author has detached from. */
783
+
detachedEmbeddingUris?: string[];
784
+
}
785
+
786
+
export type AppBskyFeedPostgateSortFields = "post" | "createdAt";
787
+
788
+
export interface AppBskyFeedPostgateDisableRule {}
789
+
790
+
export interface AppBskyFeedThreadgate {
791
+
/** Reference (AT-URI) to the post record. */
792
+
post: string;
793
+
/** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */
794
+
allow?:
795
+
| AppBskyFeedThreadgate["MentionRule"]
796
+
| AppBskyFeedThreadgate["FollowerRule"]
797
+
| AppBskyFeedThreadgate["FollowingRule"]
798
+
| AppBskyFeedThreadgate["ListRule"]
799
+
| { $type: string; [key: string]: unknown }[];
800
+
createdAt: string;
801
+
/** List of hidden reply URIs. */
802
+
hiddenReplies?: string[];
803
+
}
804
+
805
+
export type AppBskyFeedThreadgateSortFields = "post" | "createdAt";
806
+
807
+
export interface AppBskyFeedThreadgateListRule {
808
+
list: string;
809
+
}
810
+
811
+
export interface AppBskyFeedThreadgateMentionRule {}
812
+
813
+
export interface AppBskyFeedThreadgateFollowerRule {}
814
+
815
+
export interface AppBskyFeedThreadgateFollowingRule {}
816
+
817
+
export interface AppBskyRichtextFacetTag {
818
+
tag: string;
819
+
}
820
+
821
+
export interface AppBskyRichtextFacetLink {
822
+
uri: string;
823
+
}
824
+
825
+
export interface AppBskyRichtextFacetMain {
826
+
index: AppBskyRichtextFacet["ByteSlice"];
827
+
features:
828
+
| AppBskyRichtextFacet["Mention"]
829
+
| AppBskyRichtextFacet["Link"]
830
+
| AppBskyRichtextFacet["Tag"]
831
+
| { $type: string; [key: string]: unknown }[];
832
+
}
833
+
834
+
export interface AppBskyRichtextFacetMention {
835
+
did: string;
836
+
}
837
+
838
+
export interface AppBskyRichtextFacetByteSlice {
839
+
byteEnd: number;
840
+
byteStart: number;
841
+
}
842
+
843
+
export interface AppBskyActorDefsNux {
844
+
id: string;
845
+
/** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */
846
+
data?: string;
847
+
completed: boolean;
848
+
/** The date and time at which the NUX will expire and should be considered completed. */
849
+
expiresAt?: string;
850
+
}
851
+
852
+
export interface AppBskyActorDefsMutedWord {
853
+
id?: string;
854
+
/** The muted word itself. */
855
+
value: string;
856
+
/** The intended targets of the muted word. */
857
+
targets: AppBskyActorDefs["MutedWordTarget"][];
858
+
/** The date and time at which the muted word will expire and no longer be applied. */
859
+
expiresAt?: string;
860
+
/** Groups of users to apply the muted word to. If undefined, applies to all users. */
861
+
actorTarget?: AppBskyActorDefsActorTarget;
862
+
}
863
+
864
+
export interface AppBskyActorDefsSavedFeed {
865
+
id: string;
866
+
type: AppBskyActorDefsType;
867
+
value: string;
868
+
pinned: boolean;
869
+
}
870
+
871
+
export type AppBskyActorDefsPreferences =
872
+
| AppBskyActorDefs["AdultContentPref"]
873
+
| AppBskyActorDefs["ContentLabelPref"]
874
+
| AppBskyActorDefs["SavedFeedsPref"]
875
+
| AppBskyActorDefs["SavedFeedsPrefV2"]
876
+
| AppBskyActorDefs["PersonalDetailsPref"]
877
+
| AppBskyActorDefs["FeedViewPref"]
878
+
| AppBskyActorDefs["ThreadViewPref"]
879
+
| AppBskyActorDefs["InterestsPref"]
880
+
| AppBskyActorDefs["MutedWordsPref"]
881
+
| AppBskyActorDefs["HiddenPostsPref"]
882
+
| AppBskyActorDefs["BskyAppStatePref"]
883
+
| AppBskyActorDefs["LabelersPref"]
884
+
| AppBskyActorDefs["PostInteractionSettingsPref"]
885
+
| { $type: string; [key: string]: unknown }[];
886
+
887
+
export interface AppBskyActorDefsProfileView {
888
+
did: string;
889
+
avatar?: string;
890
+
handle: string;
891
+
labels?: ComAtprotoLabelDefs["Label"][];
892
+
viewer?: AppBskyActorDefs["ViewerState"];
893
+
createdAt?: string;
894
+
indexedAt?: string;
895
+
associated?: AppBskyActorDefs["ProfileAssociated"];
896
+
description?: string;
897
+
displayName?: string;
898
+
}
899
+
900
+
export interface AppBskyActorDefsViewerState {
901
+
muted?: boolean;
902
+
blocking?: string;
903
+
blockedBy?: boolean;
904
+
following?: string;
905
+
followedBy?: string;
906
+
mutedByList?: AppBskyGraphDefs["ListViewBasic"];
907
+
blockingByList?: AppBskyGraphDefs["ListViewBasic"];
908
+
knownFollowers?: AppBskyActorDefs["KnownFollowers"];
909
+
}
910
+
911
+
export interface AppBskyActorDefsFeedViewPref {
912
+
/** The URI of the feed, or an identifier which describes the feed. */
913
+
feed: string;
914
+
/** Hide replies in the feed. */
915
+
hideReplies?: boolean;
916
+
/** Hide reposts in the feed. */
917
+
hideReposts?: boolean;
918
+
/** Hide quote posts in the feed. */
919
+
hideQuotePosts?: boolean;
920
+
/** Hide replies in the feed if they do not have this number of likes. */
921
+
hideRepliesByLikeCount?: number;
922
+
/** Hide replies in the feed if they are not by followed users. */
923
+
hideRepliesByUnfollowed?: boolean;
924
+
}
925
+
926
+
export interface AppBskyActorDefsLabelersPref {
927
+
labelers: AppBskyActorDefs["LabelerPrefItem"][];
928
+
}
929
+
930
+
export interface AppBskyActorDefsInterestsPref {
931
+
/** A list of tags which describe the account owner's interests gathered during onboarding. */
932
+
tags: string[];
933
+
}
934
+
935
+
export interface AppBskyActorDefsKnownFollowers {
936
+
count: number;
937
+
followers: AppBskyActorDefs["ProfileViewBasic"][];
938
+
}
939
+
940
+
export interface AppBskyActorDefsMutedWordsPref {
941
+
/** A list of words the account owner has muted. */
942
+
items: AppBskyActorDefs["MutedWord"][];
943
+
}
944
+
945
+
export interface AppBskyActorDefsSavedFeedsPref {
946
+
saved: string[];
947
+
pinned: string[];
948
+
timelineIndex?: number;
949
+
}
950
+
951
+
export interface AppBskyActorDefsThreadViewPref {
952
+
/** Sorting mode for threads. */
953
+
sort?: AppBskyActorDefsSort;
954
+
/** Show followed users at the top of all replies. */
955
+
prioritizeFollowedUsers?: boolean;
956
+
}
957
+
958
+
export interface AppBskyActorDefsHiddenPostsPref {
959
+
/** A list of URIs of posts the account owner has hidden. */
960
+
items: string[];
961
+
}
962
+
963
+
export interface AppBskyActorDefsLabelerPrefItem {
964
+
did: string;
965
+
}
966
+
967
+
export interface AppBskyActorDefsAdultContentPref {
968
+
enabled: boolean;
969
+
}
970
+
971
+
export interface AppBskyActorDefsBskyAppStatePref {
972
+
/** Storage for NUXs the user has encountered. */
973
+
nuxs?: AppBskyActorDefs["Nux"][];
974
+
/** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */
975
+
queuedNudges?: string[];
976
+
activeProgressGuide?: AppBskyActorDefs["BskyAppProgressGuide"];
977
+
}
978
+
979
+
export interface AppBskyActorDefsContentLabelPref {
980
+
label: string;
981
+
/** Which labeler does this preference apply to? If undefined, applies globally. */
982
+
labelerDid?: string;
983
+
visibility: AppBskyActorDefsVisibility;
984
+
}
985
+
986
+
export interface AppBskyActorDefsProfileViewBasic {
987
+
did: string;
988
+
avatar?: string;
989
+
handle: string;
990
+
labels?: ComAtprotoLabelDefs["Label"][];
991
+
viewer?: AppBskyActorDefs["ViewerState"];
992
+
createdAt?: string;
993
+
associated?: AppBskyActorDefs["ProfileAssociated"];
994
+
displayName?: string;
995
+
}
996
+
997
+
export interface AppBskyActorDefsSavedFeedsPrefV2 {
998
+
items: AppBskyActorDefs["SavedFeed"][];
999
+
}
1000
+
1001
+
export interface AppBskyActorDefsProfileAssociated {
1002
+
chat?: AppBskyActorDefs["ProfileAssociatedChat"];
1003
+
lists?: number;
1004
+
labeler?: boolean;
1005
+
feedgens?: number;
1006
+
starterPacks?: number;
1007
+
}
1008
+
1009
+
export interface AppBskyActorDefsPersonalDetailsPref {
1010
+
/** The birth date of account owner. */
1011
+
birthDate?: string;
1012
+
}
1013
+
1014
+
export interface AppBskyActorDefsProfileViewDetailed {
1015
+
did: string;
1016
+
avatar?: string;
1017
+
banner?: string;
1018
+
handle: string;
1019
+
labels?: ComAtprotoLabelDefs["Label"][];
1020
+
viewer?: AppBskyActorDefs["ViewerState"];
1021
+
createdAt?: string;
1022
+
indexedAt?: string;
1023
+
associated?: AppBskyActorDefs["ProfileAssociated"];
1024
+
pinnedPost?: ComAtprotoRepoStrongRef;
1025
+
postsCount?: number;
1026
+
description?: string;
1027
+
displayName?: string;
1028
+
followsCount?: number;
1029
+
followersCount?: number;
1030
+
joinedViaStarterPack?: AppBskyGraphDefs["StarterPackViewBasic"];
1031
+
}
1032
+
1033
+
export interface AppBskyActorDefsBskyAppProgressGuide {
1034
+
guide: string;
1035
+
}
1036
+
1037
+
export interface AppBskyActorDefsProfileAssociatedChat {
1038
+
allowIncoming: AppBskyActorDefsAllowIncoming;
1039
+
}
1040
+
1041
+
export interface AppBskyActorDefsPostInteractionSettingsPref {
1042
+
/** Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */
1043
+
threadgateAllowRules?:
1044
+
| AppBskyFeedThreadgate["MentionRule"]
1045
+
| AppBskyFeedThreadgate["FollowerRule"]
1046
+
| AppBskyFeedThreadgate["FollowingRule"]
1047
+
| AppBskyFeedThreadgate["ListRule"]
1048
+
| { $type: string; [key: string]: unknown }[];
1049
+
/** Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */
1050
+
postgateEmbeddingRules?:
1051
+
| AppBskyFeedPostgate["DisableRule"]
1052
+
| {
1053
+
$type: string;
1054
+
[key: string]: unknown;
1055
+
}[];
1056
+
}
1057
+
266
1058
export interface AppBskyActorProfile {
267
1059
/** Small image to be displayed next to posts from account. AKA, 'profile picture' */
268
1060
avatar?: BlobRef;
···
288
1080
| "description"
289
1081
| "displayName";
290
1082
1083
+
export interface AppBskyLabelerDefsLabelerView {
1084
+
cid: string;
1085
+
uri: string;
1086
+
labels?: ComAtprotoLabelDefs["Label"][];
1087
+
viewer?: AppBskyLabelerDefs["LabelerViewerState"];
1088
+
creator: AppBskyActorDefs["ProfileView"];
1089
+
indexedAt: string;
1090
+
likeCount?: number;
1091
+
}
1092
+
1093
+
export interface AppBskyLabelerDefsLabelerPolicies {
1094
+
/** The label values which this labeler publishes. May include global or custom labels. */
1095
+
labelValues: ComAtprotoLabelDefs["LabelValue"][];
1096
+
/** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. */
1097
+
labelValueDefinitions?: ComAtprotoLabelDefs["LabelValueDefinition"][];
1098
+
}
1099
+
1100
+
export interface AppBskyLabelerDefsLabelerViewerState {
1101
+
like?: string;
1102
+
}
1103
+
1104
+
export interface AppBskyLabelerDefsLabelerViewDetailed {
1105
+
cid: string;
1106
+
uri: string;
1107
+
labels?: ComAtprotoLabelDefs["Label"][];
1108
+
viewer?: AppBskyLabelerDefs["LabelerViewerState"];
1109
+
creator: AppBskyActorDefs["ProfileView"];
1110
+
policies: AppBskyLabelerDefs["LabelerPolicies"];
1111
+
indexedAt: string;
1112
+
likeCount?: number;
1113
+
}
1114
+
1115
+
export interface NetworkSlicesWaitlistRequest {
1116
+
/** The AT URI of the slice being requested access to */
1117
+
slice: string;
1118
+
/** When the user joined the waitlist */
1119
+
createdAt: string;
1120
+
}
1121
+
1122
+
export type NetworkSlicesWaitlistRequestSortFields = "slice" | "createdAt";
1123
+
1124
+
export interface NetworkSlicesWaitlistDefsRequestView {
1125
+
/** The AT URI of the slice being requested access to */
1126
+
slice: string;
1127
+
/** When the user joined the waitlist */
1128
+
createdAt: string;
1129
+
/** Profile of the requester */
1130
+
profile?: AppBskyActorDefs["ProfileViewBasic"];
1131
+
}
1132
+
1133
+
export interface NetworkSlicesWaitlistDefsInviteView {
1134
+
/** The DID being invited */
1135
+
did: string;
1136
+
/** The AT URI of the slice this invite is for */
1137
+
slice: string;
1138
+
/** When this invitation was created */
1139
+
createdAt: string;
1140
+
/** Optional expiration date for this invitation */
1141
+
expiresAt?: string;
1142
+
/** The AT URI of this invite record */
1143
+
uri?: string;
1144
+
/** Profile of the invitee */
1145
+
profile?: AppBskyActorDefs["ProfileViewBasic"];
1146
+
}
1147
+
1148
+
export interface NetworkSlicesWaitlistInvite {
1149
+
/** The DID being invited */
1150
+
did: string;
1151
+
/** The AT URI of the slice this invite is for */
1152
+
slice: string;
1153
+
/** When this invitation was created */
1154
+
createdAt: string;
1155
+
/** Optional expiration date for this invitation */
1156
+
expiresAt?: string;
1157
+
}
1158
+
1159
+
export type NetworkSlicesWaitlistInviteSortFields =
1160
+
| "did"
1161
+
| "slice"
1162
+
| "createdAt"
1163
+
| "expiresAt";
1164
+
291
1165
export interface NetworkSlicesSliceDefsSliceView {
292
1166
uri: string;
293
1167
cid: string;
···
323
1197
}
324
1198
325
1199
export type NetworkSlicesSliceSortFields = "name" | "domain" | "createdAt";
326
-
327
-
export interface NetworkSlicesWaiting {
328
-
/** When the user joined the waitlist */
329
-
createdAt: string;
330
-
}
331
-
332
-
export type NetworkSlicesWaitingSortFields = "createdAt";
333
1200
334
1201
export interface NetworkSlicesLexicon {
335
1202
/** Namespaced identifier for the lexicon */
···
406
1273
407
1274
export interface ComAtprotoLabelDefsLabelValueDefinition {
408
1275
/** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */
409
-
blurs: string;
1276
+
blurs: ComAtprotoLabelDefsBlurs;
410
1277
locales: ComAtprotoLabelDefs["LabelValueDefinitionStrings"][];
411
1278
/** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */
412
-
severity: string;
1279
+
severity: ComAtprotoLabelDefsSeverity;
413
1280
/** Does the user need to have adult content enabled in order to configure this label? */
414
1281
adultOnly?: boolean;
415
1282
/** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */
416
1283
identifier: string;
417
1284
/** The default setting for this label. */
418
-
defaultSetting?: string;
1285
+
defaultSetting?: ComAtprotoLabelDefsDefaultSetting;
419
1286
}
420
1287
421
1288
export interface ComAtprotoLabelDefsLabelValueDefinitionStrings {
···
432
1299
uri: string;
433
1300
}
434
1301
1302
+
export interface AppBskyEmbedDefs {
1303
+
readonly AspectRatio: AppBskyEmbedDefsAspectRatio;
1304
+
}
1305
+
1306
+
export interface AppBskyEmbedRecord {
1307
+
readonly Main: AppBskyEmbedRecordMain;
1308
+
readonly View: AppBskyEmbedRecordView;
1309
+
readonly ViewRecord: AppBskyEmbedRecordViewRecord;
1310
+
readonly ViewBlocked: AppBskyEmbedRecordViewBlocked;
1311
+
readonly ViewDetached: AppBskyEmbedRecordViewDetached;
1312
+
readonly ViewNotFound: AppBskyEmbedRecordViewNotFound;
1313
+
}
1314
+
1315
+
export interface AppBskyEmbedImages {
1316
+
readonly Main: AppBskyEmbedImagesMain;
1317
+
readonly View: AppBskyEmbedImagesView;
1318
+
readonly Image: AppBskyEmbedImagesImage;
1319
+
readonly ViewImage: AppBskyEmbedImagesViewImage;
1320
+
}
1321
+
1322
+
export interface AppBskyEmbedRecordWithMedia {
1323
+
readonly Main: AppBskyEmbedRecordWithMediaMain;
1324
+
readonly View: AppBskyEmbedRecordWithMediaView;
1325
+
}
1326
+
1327
+
export interface AppBskyEmbedVideo {
1328
+
readonly Main: AppBskyEmbedVideoMain;
1329
+
readonly View: AppBskyEmbedVideoView;
1330
+
readonly Caption: AppBskyEmbedVideoCaption;
1331
+
}
1332
+
1333
+
export interface AppBskyEmbedExternal {
1334
+
readonly Main: AppBskyEmbedExternalMain;
1335
+
readonly View: AppBskyEmbedExternalView;
1336
+
readonly External: AppBskyEmbedExternalExternal;
1337
+
readonly ViewExternal: AppBskyEmbedExternalViewExternal;
1338
+
}
1339
+
1340
+
export interface AppBskyGraphDefs {
1341
+
readonly Modlist: AppBskyGraphDefsModlist;
1342
+
readonly ListView: AppBskyGraphDefsListView;
1343
+
readonly Curatelist: AppBskyGraphDefsCuratelist;
1344
+
readonly ListPurpose: AppBskyGraphDefsListPurpose;
1345
+
readonly ListItemView: AppBskyGraphDefsListItemView;
1346
+
readonly Relationship: AppBskyGraphDefsRelationship;
1347
+
readonly ListViewBasic: AppBskyGraphDefsListViewBasic;
1348
+
readonly NotFoundActor: AppBskyGraphDefsNotFoundActor;
1349
+
readonly Referencelist: AppBskyGraphDefsReferencelist;
1350
+
readonly ListViewerState: AppBskyGraphDefsListViewerState;
1351
+
readonly StarterPackView: AppBskyGraphDefsStarterPackView;
1352
+
readonly StarterPackViewBasic: AppBskyGraphDefsStarterPackViewBasic;
1353
+
}
1354
+
1355
+
export interface AppBskyFeedDefs {
1356
+
readonly PostView: AppBskyFeedDefsPostView;
1357
+
readonly ReplyRef: AppBskyFeedDefsReplyRef;
1358
+
readonly ReasonPin: AppBskyFeedDefsReasonPin;
1359
+
readonly BlockedPost: AppBskyFeedDefsBlockedPost;
1360
+
readonly Interaction: AppBskyFeedDefsInteraction;
1361
+
readonly RequestLess: AppBskyFeedDefsRequestLess;
1362
+
readonly RequestMore: AppBskyFeedDefsRequestMore;
1363
+
readonly ViewerState: AppBskyFeedDefsViewerState;
1364
+
readonly FeedViewPost: AppBskyFeedDefsFeedViewPost;
1365
+
readonly NotFoundPost: AppBskyFeedDefsNotFoundPost;
1366
+
readonly ReasonRepost: AppBskyFeedDefsReasonRepost;
1367
+
readonly BlockedAuthor: AppBskyFeedDefsBlockedAuthor;
1368
+
readonly GeneratorView: AppBskyFeedDefsGeneratorView;
1369
+
readonly ThreadContext: AppBskyFeedDefsThreadContext;
1370
+
readonly ThreadViewPost: AppBskyFeedDefsThreadViewPost;
1371
+
readonly ThreadgateView: AppBskyFeedDefsThreadgateView;
1372
+
readonly InteractionLike: AppBskyFeedDefsInteractionLike;
1373
+
readonly InteractionSeen: AppBskyFeedDefsInteractionSeen;
1374
+
readonly ClickthroughItem: AppBskyFeedDefsClickthroughItem;
1375
+
readonly ContentModeVideo: AppBskyFeedDefsContentModeVideo;
1376
+
readonly InteractionQuote: AppBskyFeedDefsInteractionQuote;
1377
+
readonly InteractionReply: AppBskyFeedDefsInteractionReply;
1378
+
readonly InteractionShare: AppBskyFeedDefsInteractionShare;
1379
+
readonly SkeletonFeedPost: AppBskyFeedDefsSkeletonFeedPost;
1380
+
readonly ClickthroughEmbed: AppBskyFeedDefsClickthroughEmbed;
1381
+
readonly InteractionRepost: AppBskyFeedDefsInteractionRepost;
1382
+
readonly SkeletonReasonPin: AppBskyFeedDefsSkeletonReasonPin;
1383
+
readonly ClickthroughAuthor: AppBskyFeedDefsClickthroughAuthor;
1384
+
readonly ClickthroughReposter: AppBskyFeedDefsClickthroughReposter;
1385
+
readonly GeneratorViewerState: AppBskyFeedDefsGeneratorViewerState;
1386
+
readonly SkeletonReasonRepost: AppBskyFeedDefsSkeletonReasonRepost;
1387
+
readonly ContentModeUnspecified: AppBskyFeedDefsContentModeUnspecified;
1388
+
}
1389
+
1390
+
export interface AppBskyFeedPostgate {
1391
+
readonly Main: AppBskyFeedPostgate;
1392
+
readonly DisableRule: AppBskyFeedPostgateDisableRule;
1393
+
}
1394
+
1395
+
export interface AppBskyFeedThreadgate {
1396
+
readonly Main: AppBskyFeedThreadgate;
1397
+
readonly ListRule: AppBskyFeedThreadgateListRule;
1398
+
readonly MentionRule: AppBskyFeedThreadgateMentionRule;
1399
+
readonly FollowerRule: AppBskyFeedThreadgateFollowerRule;
1400
+
readonly FollowingRule: AppBskyFeedThreadgateFollowingRule;
1401
+
}
1402
+
1403
+
export interface AppBskyRichtextFacet {
1404
+
readonly Tag: AppBskyRichtextFacetTag;
1405
+
readonly Link: AppBskyRichtextFacetLink;
1406
+
readonly Main: AppBskyRichtextFacetMain;
1407
+
readonly Mention: AppBskyRichtextFacetMention;
1408
+
readonly ByteSlice: AppBskyRichtextFacetByteSlice;
1409
+
}
1410
+
1411
+
export interface AppBskyActorDefs {
1412
+
readonly Nux: AppBskyActorDefsNux;
1413
+
readonly MutedWord: AppBskyActorDefsMutedWord;
1414
+
readonly SavedFeed: AppBskyActorDefsSavedFeed;
1415
+
readonly Preferences: AppBskyActorDefsPreferences;
1416
+
readonly ProfileView: AppBskyActorDefsProfileView;
1417
+
readonly ViewerState: AppBskyActorDefsViewerState;
1418
+
readonly FeedViewPref: AppBskyActorDefsFeedViewPref;
1419
+
readonly LabelersPref: AppBskyActorDefsLabelersPref;
1420
+
readonly InterestsPref: AppBskyActorDefsInterestsPref;
1421
+
readonly KnownFollowers: AppBskyActorDefsKnownFollowers;
1422
+
readonly MutedWordsPref: AppBskyActorDefsMutedWordsPref;
1423
+
readonly SavedFeedsPref: AppBskyActorDefsSavedFeedsPref;
1424
+
readonly ThreadViewPref: AppBskyActorDefsThreadViewPref;
1425
+
readonly HiddenPostsPref: AppBskyActorDefsHiddenPostsPref;
1426
+
readonly LabelerPrefItem: AppBskyActorDefsLabelerPrefItem;
1427
+
readonly MutedWordTarget: AppBskyActorDefsMutedWordTarget;
1428
+
readonly AdultContentPref: AppBskyActorDefsAdultContentPref;
1429
+
readonly BskyAppStatePref: AppBskyActorDefsBskyAppStatePref;
1430
+
readonly ContentLabelPref: AppBskyActorDefsContentLabelPref;
1431
+
readonly ProfileViewBasic: AppBskyActorDefsProfileViewBasic;
1432
+
readonly SavedFeedsPrefV2: AppBskyActorDefsSavedFeedsPrefV2;
1433
+
readonly ProfileAssociated: AppBskyActorDefsProfileAssociated;
1434
+
readonly PersonalDetailsPref: AppBskyActorDefsPersonalDetailsPref;
1435
+
readonly ProfileViewDetailed: AppBskyActorDefsProfileViewDetailed;
1436
+
readonly BskyAppProgressGuide: AppBskyActorDefsBskyAppProgressGuide;
1437
+
readonly ProfileAssociatedChat: AppBskyActorDefsProfileAssociatedChat;
1438
+
readonly PostInteractionSettingsPref: AppBskyActorDefsPostInteractionSettingsPref;
1439
+
}
1440
+
1441
+
export interface AppBskyLabelerDefs {
1442
+
readonly LabelerView: AppBskyLabelerDefsLabelerView;
1443
+
readonly LabelerPolicies: AppBskyLabelerDefsLabelerPolicies;
1444
+
readonly LabelerViewerState: AppBskyLabelerDefsLabelerViewerState;
1445
+
readonly LabelerViewDetailed: AppBskyLabelerDefsLabelerViewDetailed;
1446
+
}
1447
+
1448
+
export interface NetworkSlicesWaitlistDefs {
1449
+
readonly RequestView: NetworkSlicesWaitlistDefsRequestView;
1450
+
readonly InviteView: NetworkSlicesWaitlistDefsInviteView;
1451
+
}
1452
+
435
1453
export interface NetworkSlicesSliceDefs {
436
1454
readonly SliceView: NetworkSlicesSliceDefsSliceView;
437
1455
readonly SparklinePoint: NetworkSlicesSliceDefsSparklinePoint;
···
444
1462
export interface ComAtprotoLabelDefs {
445
1463
readonly Label: ComAtprotoLabelDefsLabel;
446
1464
readonly SelfLabel: ComAtprotoLabelDefsSelfLabel;
1465
+
readonly LabelValue: ComAtprotoLabelDefsLabelValue;
447
1466
readonly SelfLabels: ComAtprotoLabelDefsSelfLabels;
448
1467
readonly LabelValueDefinition: ComAtprotoLabelDefsLabelValueDefinition;
449
1468
readonly LabelValueDefinitionStrings: ComAtprotoLabelDefsLabelValueDefinitionStrings;
450
1469
}
451
1470
1471
+
class FollowGraphBskyAppClient {
1472
+
private readonly client: SlicesClient;
1473
+
1474
+
constructor(client: SlicesClient) {
1475
+
this.client = client;
1476
+
}
1477
+
1478
+
async getRecords(params?: {
1479
+
limit?: number;
1480
+
cursor?: string;
1481
+
where?: {
1482
+
[K in
1483
+
| AppBskyGraphFollowSortFields
1484
+
| IndexedRecordFields]?: WhereCondition;
1485
+
};
1486
+
orWhere?: {
1487
+
[K in
1488
+
| AppBskyGraphFollowSortFields
1489
+
| IndexedRecordFields]?: WhereCondition;
1490
+
};
1491
+
sortBy?: SortField<AppBskyGraphFollowSortFields>[];
1492
+
}): Promise<GetRecordsResponse<AppBskyGraphFollow>> {
1493
+
return await this.client.getRecords("app.bsky.graph.follow", params);
1494
+
}
1495
+
1496
+
async getRecord(
1497
+
params: GetRecordParams
1498
+
): Promise<RecordResponse<AppBskyGraphFollow>> {
1499
+
return await this.client.getRecord("app.bsky.graph.follow", params);
1500
+
}
1501
+
1502
+
async countRecords(params?: {
1503
+
limit?: number;
1504
+
cursor?: string;
1505
+
where?: {
1506
+
[K in
1507
+
| AppBskyGraphFollowSortFields
1508
+
| IndexedRecordFields]?: WhereCondition;
1509
+
};
1510
+
orWhere?: {
1511
+
[K in
1512
+
| AppBskyGraphFollowSortFields
1513
+
| IndexedRecordFields]?: WhereCondition;
1514
+
};
1515
+
sortBy?: SortField<AppBskyGraphFollowSortFields>[];
1516
+
}): Promise<CountRecordsResponse> {
1517
+
return await this.client.countRecords("app.bsky.graph.follow", params);
1518
+
}
1519
+
1520
+
async createRecord(
1521
+
record: AppBskyGraphFollow,
1522
+
useSelfRkey?: boolean
1523
+
): Promise<{ uri: string; cid: string }> {
1524
+
return await this.client.createRecord(
1525
+
"app.bsky.graph.follow",
1526
+
record,
1527
+
useSelfRkey
1528
+
);
1529
+
}
1530
+
1531
+
async updateRecord(
1532
+
rkey: string,
1533
+
record: AppBskyGraphFollow
1534
+
): Promise<{ uri: string; cid: string }> {
1535
+
return await this.client.updateRecord(
1536
+
"app.bsky.graph.follow",
1537
+
rkey,
1538
+
record
1539
+
);
1540
+
}
1541
+
1542
+
async deleteRecord(rkey: string): Promise<void> {
1543
+
return await this.client.deleteRecord("app.bsky.graph.follow", rkey);
1544
+
}
1545
+
}
1546
+
1547
+
class GraphBskyAppClient {
1548
+
readonly follow: FollowGraphBskyAppClient;
1549
+
private readonly client: SlicesClient;
1550
+
1551
+
constructor(client: SlicesClient) {
1552
+
this.client = client;
1553
+
this.follow = new FollowGraphBskyAppClient(client);
1554
+
}
1555
+
}
1556
+
1557
+
class PostgateFeedBskyAppClient {
1558
+
private readonly client: SlicesClient;
1559
+
1560
+
constructor(client: SlicesClient) {
1561
+
this.client = client;
1562
+
}
1563
+
1564
+
async getRecords(params?: {
1565
+
limit?: number;
1566
+
cursor?: string;
1567
+
where?: {
1568
+
[K in
1569
+
| AppBskyFeedPostgateSortFields
1570
+
| IndexedRecordFields]?: WhereCondition;
1571
+
};
1572
+
orWhere?: {
1573
+
[K in
1574
+
| AppBskyFeedPostgateSortFields
1575
+
| IndexedRecordFields]?: WhereCondition;
1576
+
};
1577
+
sortBy?: SortField<AppBskyFeedPostgateSortFields>[];
1578
+
}): Promise<GetRecordsResponse<AppBskyFeedPostgate>> {
1579
+
return await this.client.getRecords("app.bsky.feed.postgate", params);
1580
+
}
1581
+
1582
+
async getRecord(
1583
+
params: GetRecordParams
1584
+
): Promise<RecordResponse<AppBskyFeedPostgate>> {
1585
+
return await this.client.getRecord("app.bsky.feed.postgate", params);
1586
+
}
1587
+
1588
+
async countRecords(params?: {
1589
+
limit?: number;
1590
+
cursor?: string;
1591
+
where?: {
1592
+
[K in
1593
+
| AppBskyFeedPostgateSortFields
1594
+
| IndexedRecordFields]?: WhereCondition;
1595
+
};
1596
+
orWhere?: {
1597
+
[K in
1598
+
| AppBskyFeedPostgateSortFields
1599
+
| IndexedRecordFields]?: WhereCondition;
1600
+
};
1601
+
sortBy?: SortField<AppBskyFeedPostgateSortFields>[];
1602
+
}): Promise<CountRecordsResponse> {
1603
+
return await this.client.countRecords("app.bsky.feed.postgate", params);
1604
+
}
1605
+
1606
+
async createRecord(
1607
+
record: AppBskyFeedPostgate,
1608
+
useSelfRkey?: boolean
1609
+
): Promise<{ uri: string; cid: string }> {
1610
+
return await this.client.createRecord(
1611
+
"app.bsky.feed.postgate",
1612
+
record,
1613
+
useSelfRkey
1614
+
);
1615
+
}
1616
+
1617
+
async updateRecord(
1618
+
rkey: string,
1619
+
record: AppBskyFeedPostgate
1620
+
): Promise<{ uri: string; cid: string }> {
1621
+
return await this.client.updateRecord(
1622
+
"app.bsky.feed.postgate",
1623
+
rkey,
1624
+
record
1625
+
);
1626
+
}
1627
+
1628
+
async deleteRecord(rkey: string): Promise<void> {
1629
+
return await this.client.deleteRecord("app.bsky.feed.postgate", rkey);
1630
+
}
1631
+
}
1632
+
1633
+
class ThreadgateFeedBskyAppClient {
1634
+
private readonly client: SlicesClient;
1635
+
1636
+
constructor(client: SlicesClient) {
1637
+
this.client = client;
1638
+
}
1639
+
1640
+
async getRecords(params?: {
1641
+
limit?: number;
1642
+
cursor?: string;
1643
+
where?: {
1644
+
[K in
1645
+
| AppBskyFeedThreadgateSortFields
1646
+
| IndexedRecordFields]?: WhereCondition;
1647
+
};
1648
+
orWhere?: {
1649
+
[K in
1650
+
| AppBskyFeedThreadgateSortFields
1651
+
| IndexedRecordFields]?: WhereCondition;
1652
+
};
1653
+
sortBy?: SortField<AppBskyFeedThreadgateSortFields>[];
1654
+
}): Promise<GetRecordsResponse<AppBskyFeedThreadgate>> {
1655
+
return await this.client.getRecords("app.bsky.feed.threadgate", params);
1656
+
}
1657
+
1658
+
async getRecord(
1659
+
params: GetRecordParams
1660
+
): Promise<RecordResponse<AppBskyFeedThreadgate>> {
1661
+
return await this.client.getRecord("app.bsky.feed.threadgate", params);
1662
+
}
1663
+
1664
+
async countRecords(params?: {
1665
+
limit?: number;
1666
+
cursor?: string;
1667
+
where?: {
1668
+
[K in
1669
+
| AppBskyFeedThreadgateSortFields
1670
+
| IndexedRecordFields]?: WhereCondition;
1671
+
};
1672
+
orWhere?: {
1673
+
[K in
1674
+
| AppBskyFeedThreadgateSortFields
1675
+
| IndexedRecordFields]?: WhereCondition;
1676
+
};
1677
+
sortBy?: SortField<AppBskyFeedThreadgateSortFields>[];
1678
+
}): Promise<CountRecordsResponse> {
1679
+
return await this.client.countRecords("app.bsky.feed.threadgate", params);
1680
+
}
1681
+
1682
+
async createRecord(
1683
+
record: AppBskyFeedThreadgate,
1684
+
useSelfRkey?: boolean
1685
+
): Promise<{ uri: string; cid: string }> {
1686
+
return await this.client.createRecord(
1687
+
"app.bsky.feed.threadgate",
1688
+
record,
1689
+
useSelfRkey
1690
+
);
1691
+
}
1692
+
1693
+
async updateRecord(
1694
+
rkey: string,
1695
+
record: AppBskyFeedThreadgate
1696
+
): Promise<{ uri: string; cid: string }> {
1697
+
return await this.client.updateRecord(
1698
+
"app.bsky.feed.threadgate",
1699
+
rkey,
1700
+
record
1701
+
);
1702
+
}
1703
+
1704
+
async deleteRecord(rkey: string): Promise<void> {
1705
+
return await this.client.deleteRecord("app.bsky.feed.threadgate", rkey);
1706
+
}
1707
+
}
1708
+
1709
+
class FeedBskyAppClient {
1710
+
readonly postgate: PostgateFeedBskyAppClient;
1711
+
readonly threadgate: ThreadgateFeedBskyAppClient;
1712
+
private readonly client: SlicesClient;
1713
+
1714
+
constructor(client: SlicesClient) {
1715
+
this.client = client;
1716
+
this.postgate = new PostgateFeedBskyAppClient(client);
1717
+
this.threadgate = new ThreadgateFeedBskyAppClient(client);
1718
+
}
1719
+
}
1720
+
452
1721
class ProfileActorBskyAppClient {
453
1722
private readonly client: SlicesClient;
454
1723
···
536
1805
}
537
1806
538
1807
class BskyAppClient {
1808
+
readonly graph: GraphBskyAppClient;
1809
+
readonly feed: FeedBskyAppClient;
539
1810
readonly actor: ActorBskyAppClient;
540
1811
private readonly client: SlicesClient;
541
1812
542
1813
constructor(client: SlicesClient) {
543
1814
this.client = client;
1815
+
this.graph = new GraphBskyAppClient(client);
1816
+
this.feed = new FeedBskyAppClient(client);
544
1817
this.actor = new ActorBskyAppClient(client);
545
1818
}
546
1819
}
···
555
1828
}
556
1829
}
557
1830
1831
+
class RequestWaitlistSlicesNetworkClient {
1832
+
private readonly client: SlicesClient;
1833
+
1834
+
constructor(client: SlicesClient) {
1835
+
this.client = client;
1836
+
}
1837
+
1838
+
async getRecords(params?: {
1839
+
limit?: number;
1840
+
cursor?: string;
1841
+
where?: {
1842
+
[K in
1843
+
| NetworkSlicesWaitlistRequestSortFields
1844
+
| IndexedRecordFields]?: WhereCondition;
1845
+
};
1846
+
orWhere?: {
1847
+
[K in
1848
+
| NetworkSlicesWaitlistRequestSortFields
1849
+
| IndexedRecordFields]?: WhereCondition;
1850
+
};
1851
+
sortBy?: SortField<NetworkSlicesWaitlistRequestSortFields>[];
1852
+
}): Promise<GetRecordsResponse<NetworkSlicesWaitlistRequest>> {
1853
+
return await this.client.getRecords(
1854
+
"network.slices.waitlist.request",
1855
+
params
1856
+
);
1857
+
}
1858
+
1859
+
async getRecord(
1860
+
params: GetRecordParams
1861
+
): Promise<RecordResponse<NetworkSlicesWaitlistRequest>> {
1862
+
return await this.client.getRecord(
1863
+
"network.slices.waitlist.request",
1864
+
params
1865
+
);
1866
+
}
1867
+
1868
+
async countRecords(params?: {
1869
+
limit?: number;
1870
+
cursor?: string;
1871
+
where?: {
1872
+
[K in
1873
+
| NetworkSlicesWaitlistRequestSortFields
1874
+
| IndexedRecordFields]?: WhereCondition;
1875
+
};
1876
+
orWhere?: {
1877
+
[K in
1878
+
| NetworkSlicesWaitlistRequestSortFields
1879
+
| IndexedRecordFields]?: WhereCondition;
1880
+
};
1881
+
sortBy?: SortField<NetworkSlicesWaitlistRequestSortFields>[];
1882
+
}): Promise<CountRecordsResponse> {
1883
+
return await this.client.countRecords(
1884
+
"network.slices.waitlist.request",
1885
+
params
1886
+
);
1887
+
}
1888
+
1889
+
async createRecord(
1890
+
record: NetworkSlicesWaitlistRequest,
1891
+
useSelfRkey?: boolean
1892
+
): Promise<{ uri: string; cid: string }> {
1893
+
return await this.client.createRecord(
1894
+
"network.slices.waitlist.request",
1895
+
record,
1896
+
useSelfRkey
1897
+
);
1898
+
}
1899
+
1900
+
async updateRecord(
1901
+
rkey: string,
1902
+
record: NetworkSlicesWaitlistRequest
1903
+
): Promise<{ uri: string; cid: string }> {
1904
+
return await this.client.updateRecord(
1905
+
"network.slices.waitlist.request",
1906
+
rkey,
1907
+
record
1908
+
);
1909
+
}
1910
+
1911
+
async deleteRecord(rkey: string): Promise<void> {
1912
+
return await this.client.deleteRecord(
1913
+
"network.slices.waitlist.request",
1914
+
rkey
1915
+
);
1916
+
}
1917
+
}
1918
+
1919
+
class InviteWaitlistSlicesNetworkClient {
1920
+
private readonly client: SlicesClient;
1921
+
1922
+
constructor(client: SlicesClient) {
1923
+
this.client = client;
1924
+
}
1925
+
1926
+
async getRecords(params?: {
1927
+
limit?: number;
1928
+
cursor?: string;
1929
+
where?: {
1930
+
[K in
1931
+
| NetworkSlicesWaitlistInviteSortFields
1932
+
| IndexedRecordFields]?: WhereCondition;
1933
+
};
1934
+
orWhere?: {
1935
+
[K in
1936
+
| NetworkSlicesWaitlistInviteSortFields
1937
+
| IndexedRecordFields]?: WhereCondition;
1938
+
};
1939
+
sortBy?: SortField<NetworkSlicesWaitlistInviteSortFields>[];
1940
+
}): Promise<GetRecordsResponse<NetworkSlicesWaitlistInvite>> {
1941
+
return await this.client.getRecords(
1942
+
"network.slices.waitlist.invite",
1943
+
params
1944
+
);
1945
+
}
1946
+
1947
+
async getRecord(
1948
+
params: GetRecordParams
1949
+
): Promise<RecordResponse<NetworkSlicesWaitlistInvite>> {
1950
+
return await this.client.getRecord(
1951
+
"network.slices.waitlist.invite",
1952
+
params
1953
+
);
1954
+
}
1955
+
1956
+
async countRecords(params?: {
1957
+
limit?: number;
1958
+
cursor?: string;
1959
+
where?: {
1960
+
[K in
1961
+
| NetworkSlicesWaitlistInviteSortFields
1962
+
| IndexedRecordFields]?: WhereCondition;
1963
+
};
1964
+
orWhere?: {
1965
+
[K in
1966
+
| NetworkSlicesWaitlistInviteSortFields
1967
+
| IndexedRecordFields]?: WhereCondition;
1968
+
};
1969
+
sortBy?: SortField<NetworkSlicesWaitlistInviteSortFields>[];
1970
+
}): Promise<CountRecordsResponse> {
1971
+
return await this.client.countRecords(
1972
+
"network.slices.waitlist.invite",
1973
+
params
1974
+
);
1975
+
}
1976
+
1977
+
async createRecord(
1978
+
record: NetworkSlicesWaitlistInvite,
1979
+
useSelfRkey?: boolean
1980
+
): Promise<{ uri: string; cid: string }> {
1981
+
return await this.client.createRecord(
1982
+
"network.slices.waitlist.invite",
1983
+
record,
1984
+
useSelfRkey
1985
+
);
1986
+
}
1987
+
1988
+
async updateRecord(
1989
+
rkey: string,
1990
+
record: NetworkSlicesWaitlistInvite
1991
+
): Promise<{ uri: string; cid: string }> {
1992
+
return await this.client.updateRecord(
1993
+
"network.slices.waitlist.invite",
1994
+
rkey,
1995
+
record
1996
+
);
1997
+
}
1998
+
1999
+
async deleteRecord(rkey: string): Promise<void> {
2000
+
return await this.client.deleteRecord(
2001
+
"network.slices.waitlist.invite",
2002
+
rkey
2003
+
);
2004
+
}
2005
+
}
2006
+
2007
+
class WaitlistSlicesNetworkClient {
2008
+
readonly request: RequestWaitlistSlicesNetworkClient;
2009
+
readonly invite: InviteWaitlistSlicesNetworkClient;
2010
+
private readonly client: SlicesClient;
2011
+
2012
+
constructor(client: SlicesClient) {
2013
+
this.client = client;
2014
+
this.request = new RequestWaitlistSlicesNetworkClient(client);
2015
+
this.invite = new InviteWaitlistSlicesNetworkClient(client);
2016
+
}
2017
+
}
2018
+
558
2019
class SliceSlicesNetworkClient {
559
2020
private readonly client: SlicesClient;
560
2021
···
784
2245
}
785
2246
}
786
2247
787
-
class WaitingSlicesNetworkClient {
788
-
private readonly client: SlicesClient;
789
-
790
-
constructor(client: SlicesClient) {
791
-
this.client = client;
792
-
}
793
-
794
-
async getRecords(params?: {
795
-
limit?: number;
796
-
cursor?: string;
797
-
where?: {
798
-
[K in
799
-
| NetworkSlicesWaitingSortFields
800
-
| IndexedRecordFields]?: WhereCondition;
801
-
};
802
-
orWhere?: {
803
-
[K in
804
-
| NetworkSlicesWaitingSortFields
805
-
| IndexedRecordFields]?: WhereCondition;
806
-
};
807
-
sortBy?: SortField<NetworkSlicesWaitingSortFields>[];
808
-
}): Promise<GetRecordsResponse<NetworkSlicesWaiting>> {
809
-
return await this.client.getRecords("network.slices.waiting", params);
810
-
}
811
-
812
-
async getRecord(
813
-
params: GetRecordParams
814
-
): Promise<RecordResponse<NetworkSlicesWaiting>> {
815
-
return await this.client.getRecord("network.slices.waiting", params);
816
-
}
817
-
818
-
async countRecords(params?: {
819
-
limit?: number;
820
-
cursor?: string;
821
-
where?: {
822
-
[K in
823
-
| NetworkSlicesWaitingSortFields
824
-
| IndexedRecordFields]?: WhereCondition;
825
-
};
826
-
orWhere?: {
827
-
[K in
828
-
| NetworkSlicesWaitingSortFields
829
-
| IndexedRecordFields]?: WhereCondition;
830
-
};
831
-
sortBy?: SortField<NetworkSlicesWaitingSortFields>[];
832
-
}): Promise<CountRecordsResponse> {
833
-
return await this.client.countRecords("network.slices.waiting", params);
834
-
}
835
-
836
-
async createRecord(
837
-
record: NetworkSlicesWaiting,
838
-
useSelfRkey?: boolean
839
-
): Promise<{ uri: string; cid: string }> {
840
-
return await this.client.createRecord(
841
-
"network.slices.waiting",
842
-
record,
843
-
useSelfRkey
844
-
);
845
-
}
846
-
847
-
async updateRecord(
848
-
rkey: string,
849
-
record: NetworkSlicesWaiting
850
-
): Promise<{ uri: string; cid: string }> {
851
-
return await this.client.updateRecord(
852
-
"network.slices.waiting",
853
-
rkey,
854
-
record
855
-
);
856
-
}
857
-
858
-
async deleteRecord(rkey: string): Promise<void> {
859
-
return await this.client.deleteRecord("network.slices.waiting", rkey);
860
-
}
861
-
}
862
-
863
2248
class LexiconSlicesNetworkClient {
864
2249
private readonly client: SlicesClient;
865
2250
···
1026
2411
}
1027
2412
1028
2413
class SlicesNetworkClient {
2414
+
readonly waitlist: WaitlistSlicesNetworkClient;
1029
2415
readonly slice: SliceSlicesNetworkClient;
1030
-
readonly waiting: WaitingSlicesNetworkClient;
1031
2416
readonly lexicon: LexiconSlicesNetworkClient;
1032
2417
readonly actor: ActorSlicesNetworkClient;
1033
2418
private readonly client: SlicesClient;
1034
2419
1035
2420
constructor(client: SlicesClient) {
1036
2421
this.client = client;
2422
+
this.waitlist = new WaitlistSlicesNetworkClient(client);
1037
2423
this.slice = new SliceSlicesNetworkClient(client);
1038
-
this.waiting = new WaitingSlicesNetworkClient(client);
1039
2424
this.lexicon = new LexiconSlicesNetworkClient(client);
1040
2425
this.actor = new ActorSlicesNetworkClient(client);
1041
2426
}
+3
-3
frontend/src/config.ts
+3
-3
frontend/src/config.ts
···
7
7
const OAUTH_REDIRECT_URI = Deno.env.get("OAUTH_REDIRECT_URI");
8
8
const OAUTH_AIP_BASE_URL = Deno.env.get("OAUTH_AIP_BASE_URL");
9
9
const API_URL = Deno.env.get("API_URL");
10
-
const SLICE_URI = Deno.env.get("SLICE_URI");
10
+
export const SLICE_URI = Deno.env.get("SLICE_URI");
11
11
12
12
if (
13
13
!OAUTH_CLIENT_ID ||
···
19
19
) {
20
20
throw new Error(
21
21
"Missing OAuth configuration. Please ensure .env file contains:\n" +
22
-
"OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_AIP_BASE_URL, API_URL, SLICE_URI",
22
+
"OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_AIP_BASE_URL, API_URL, SLICE_URI"
23
23
);
24
24
}
25
25
···
47
47
// "repo:network.slices.waiting",
48
48
],
49
49
},
50
-
oauthStorage,
50
+
oauthStorage
51
51
);
52
52
53
53
// Session setup (shared database)
+178
-57
frontend/src/features/auth/handlers.tsx
+178
-57
frontend/src/features/auth/handlers.tsx
···
3
3
import { atprotoClient, oauthSessions, sessionStore } from "../../config.ts";
4
4
import { renderHTML } from "../../utils/render.tsx";
5
5
import { LoginPage } from "./templates/LoginPage.tsx";
6
+
import { SLICE_URI } from "../../config.ts";
7
+
import type { NetworkSlicesActorProfile } from "../../client.ts";
8
+
9
+
// ============================================================================
10
+
// WAITLIST GATING UTILITIES
11
+
// ============================================================================
12
+
13
+
async function checkUserAccess(
14
+
userDid: string
15
+
): Promise<{ hasAccess: boolean; isOnWaitlist: boolean }> {
16
+
try {
17
+
// Build the slice URI to query invites for
18
+
const sliceUri = SLICE_URI!;
19
+
20
+
// Query for invites for this DID - using json field to query the record content
21
+
const invitesResult =
22
+
await atprotoClient.network.slices.waitlist.invite.getRecords({
23
+
where: {
24
+
slice: { eq: sliceUri },
25
+
json: { contains: userDid },
26
+
},
27
+
limit: 1,
28
+
});
29
+
30
+
// Check if user has a valid invite
31
+
if (invitesResult.records && invitesResult.records.length > 0) {
32
+
const invite = invitesResult.records[0];
33
+
34
+
// Check if invite has expired
35
+
if (invite.value.expiresAt) {
36
+
const expiresAt = new Date(invite.value.expiresAt);
37
+
const now = new Date();
38
+
if (expiresAt < now) {
39
+
return { hasAccess: false, isOnWaitlist: false }; // Invite has expired
40
+
}
41
+
}
42
+
43
+
return { hasAccess: true, isOnWaitlist: false }; // Valid invite found
44
+
}
45
+
46
+
// Check if user is already on the waitlist - requests are created by the user so record.did is correct
47
+
const requestsResult =
48
+
await atprotoClient.network.slices.waitlist.request.getRecords({
49
+
where: {
50
+
slice: { eq: sliceUri },
51
+
json: { eq: userDid },
52
+
},
53
+
limit: 1,
54
+
});
55
+
56
+
const isOnWaitlist =
57
+
requestsResult.records && requestsResult.records.length > 0;
58
+
59
+
return { hasAccess: false, isOnWaitlist };
60
+
} catch (error) {
61
+
console.error("Error checking user access:", error);
62
+
return { hasAccess: false, isOnWaitlist: false }; // Default to blocking access on error
63
+
}
64
+
}
6
65
7
66
// ============================================================================
8
67
// LOGIN PAGE HANDLER
···
14
73
15
74
const error = url.searchParams.get("error");
16
75
return renderHTML(
17
-
<LoginPage error={error || undefined} currentUser={context.currentUser} />,
76
+
<LoginPage error={error || undefined} currentUser={context.currentUser} />
18
77
);
19
78
}
20
79
···
49
108
return Response.redirect(
50
109
new URL(
51
110
"/login?error=" + encodeURIComponent("OAuth initialization failed"),
52
-
req.url,
111
+
req.url
53
112
),
54
-
302,
113
+
302
55
114
);
56
115
}
57
116
}
···
82
141
return Response.redirect(
83
142
new URL(
84
143
"/login?error=" + encodeURIComponent("Invalid OAuth callback"),
85
-
req.url,
144
+
req.url
86
145
),
87
-
302,
146
+
302
88
147
);
89
148
}
90
149
···
92
151
return Response.redirect(
93
152
new URL(
94
153
"/login?error=" + encodeURIComponent("OAuth client not configured"),
95
-
req.url,
154
+
req.url
96
155
),
97
-
302,
156
+
302
98
157
);
99
158
}
100
159
···
111
170
return Response.redirect(
112
171
new URL(
113
172
"/login?error=" + encodeURIComponent("Failed to create session"),
114
-
req.url,
173
+
req.url
115
174
),
116
-
302,
175
+
302
117
176
);
118
177
}
119
178
···
125
184
try {
126
185
userInfo = await atprotoClient.oauth?.getUserInfo();
127
186
} catch (error) {
128
-
console.log("Failed to get user info:", error);
187
+
console.error("Failed to get user info:", error);
188
+
}
189
+
190
+
// Check waitlist access if user info is available
191
+
if (userInfo?.sub) {
192
+
const { hasAccess, isOnWaitlist } = await checkUserAccess(userInfo.sub);
193
+
if (!hasAccess) {
194
+
// Clear OAuth session and redirect to waitlist page
195
+
await atprotoClient.oauth?.logout();
196
+
197
+
const errorCode = isOnWaitlist
198
+
? "already_on_waitlist"
199
+
: "invite_required";
200
+
return Response.redirect(
201
+
new URL(`/waitlist?error=${errorCode}`, req.url),
202
+
302
203
+
);
204
+
}
129
205
}
130
206
131
-
// Sync external collections if user doesn't have them yet
207
+
// Sync external collections first to ensure actor records are populated
132
208
try {
133
-
if (!userInfo?.sub) {
134
-
console.log(
135
-
"No user DID available, skipping external collections sync",
136
-
);
137
-
} else {
138
-
// Check if user already has bsky profile synced
139
-
try {
140
-
const profileCheck = await atprotoClient.app.bsky.actor.profile
141
-
.getRecords({
142
-
where: {
143
-
did: { eq: userInfo.sub },
144
-
},
145
-
limit: 1,
146
-
});
209
+
if (userInfo?.sub) {
210
+
await atprotoClient.network.slices.slice.syncUserCollections();
211
+
}
212
+
} catch (error) {
213
+
console.error("Error during external collections sync:", error);
214
+
}
215
+
216
+
// Create network.slices.actor.profile record for first-time users
217
+
if (userInfo?.sub && userInfo?.name) {
218
+
try {
219
+
// Check if user already has a profile record in our slice
220
+
const existingProfile =
221
+
await atprotoClient.network.slices.actor.profile.getRecords({
222
+
where: {
223
+
did: { eq: userInfo.sub },
224
+
},
225
+
limit: 1,
226
+
});
227
+
228
+
// If no profile exists, create one
229
+
if (!existingProfile.records || existingProfile.records.length === 0) {
230
+
// Fetch their bsky profile to copy avatar and other data
231
+
const profileData: NetworkSlicesActorProfile = {
232
+
displayName: userInfo.name || userInfo.sub,
233
+
createdAt: new Date().toISOString(),
234
+
};
235
+
236
+
try {
237
+
// Get their bsky profile data
238
+
const bskyProfile =
239
+
await atprotoClient.app.bsky.actor.profile.getRecords({
240
+
where: {
241
+
did: { eq: userInfo.sub },
242
+
},
243
+
limit: 1,
244
+
});
245
+
246
+
if (bskyProfile.records && bskyProfile.records.length > 0) {
247
+
const bskyData = bskyProfile.records[0].value;
147
248
148
-
// If we can't find existing records, sync them
149
-
if (!profileCheck.records || profileCheck.records.length === 0) {
150
-
console.log("No existing external collections found, syncing...");
151
-
await atprotoClient.network.slices.slice.syncUserCollections();
152
-
} else {
153
-
console.log("External collections already synced, skipping sync");
249
+
// Copy over relevant fields from bsky profile
250
+
if (bskyData.displayName) {
251
+
profileData.displayName = bskyData.displayName;
252
+
}
253
+
if (bskyData.description) {
254
+
profileData.description = bskyData.description;
255
+
}
256
+
if (bskyData.avatar) {
257
+
profileData.avatar = bskyData.avatar;
258
+
}
259
+
}
260
+
} catch (_bskyError) {
261
+
// Could not fetch bsky profile, using basic data
154
262
}
155
-
} catch (_profileError) {
156
-
// If we can't check existing records, skip sync to be safe
157
-
console.log(
158
-
"Could not check existing external collections, skipping sync",
263
+
264
+
// Create the profile record with the data using "self" as the rkey
265
+
await atprotoClient.network.slices.actor.profile.createRecord(
266
+
profileData,
267
+
true
159
268
);
160
269
}
270
+
} catch (error) {
271
+
console.error(
272
+
"Error creating network.slices.actor.profile record:",
273
+
error
274
+
);
275
+
// Don't fail the login process if profile creation fails
161
276
}
162
-
} catch (error) {
163
-
console.log(
164
-
"Error during sync check, skipping external collections sync:",
165
-
error,
166
-
);
167
277
}
168
278
169
279
// Redirect to user's profile page if handle is available
···
181
291
return Response.redirect(
182
292
new URL(
183
293
"/login?error=" + encodeURIComponent("Authentication failed"),
184
-
req.url,
294
+
req.url
185
295
),
186
-
302,
296
+
302
187
297
);
188
298
}
189
299
}
···
235
345
isWaitlistFlow: true,
236
346
handle,
237
347
redirectUri: "/auth/waitlist/callback",
238
-
}),
348
+
})
239
349
);
240
350
241
351
// Initiate OAuth with minimal scope for waitlist, passing state directly
242
352
const authResult = await atprotoClient.oauth.authorize({
243
353
loginHint: handle,
244
-
scope: "atproto repo:network.slices.waiting",
354
+
scope: "atproto repo:network.slices.waitlist.request",
245
355
state: waitlistState,
246
356
});
247
357
···
263
373
if (!code || !state) {
264
374
return Response.redirect(
265
375
new URL("/waitlist?error=invalid_callback", req.url),
266
-
302,
376
+
302
267
377
);
268
378
}
269
379
···
282
392
if (!atprotoClient.oauth) {
283
393
return Response.redirect(
284
394
new URL("/waitlist?error=oauth_not_configured", req.url),
285
-
302,
395
+
302
286
396
);
287
397
}
288
398
···
293
403
const userInfo = await atprotoClient.oauth.getUserInfo();
294
404
295
405
if (!userInfo) {
296
-
return Response.redirect(new URL("/waitlist?error=no_user_info", req.url), 302);
406
+
return Response.redirect(
407
+
new URL("/waitlist?error=no_user_info", req.url),
408
+
302
409
+
);
297
410
}
298
411
299
412
// Create waitlist record
300
413
try {
301
-
// For now, just log the waitlist join
302
-
console.log("User joined waitlist:", {
303
-
did: userInfo.sub,
304
-
handle: userInfo.name || waitlistData.handle || "unknown",
305
-
joinedAt: new Date().toISOString(),
306
-
});
307
-
308
-
await atprotoClient.network.slices.waiting.createRecord(
414
+
await atprotoClient.network.slices.waitlist.request.createRecord(
309
415
{
416
+
slice: SLICE_URI!,
310
417
createdAt: new Date().toISOString(),
311
418
},
312
-
true,
419
+
true
313
420
);
421
+
422
+
// Sync user collections to populate their Bluesky profile data
423
+
try {
424
+
await atprotoClient.network.slices.slice.syncUserCollections();
425
+
} catch (syncError) {
426
+
console.error(
427
+
"Failed to sync user collections for waitlist user:",
428
+
syncError
429
+
);
430
+
// Don't fail the waitlist process if sync fails
431
+
}
314
432
} catch (error) {
315
433
console.error("Failed to create waitlist record:", error);
316
434
}
···
326
444
return Response.redirect(redirectUrl.toString(), 302);
327
445
} catch (error) {
328
446
console.error("Waitlist callback error:", error);
329
-
return Response.redirect(new URL("/waitlist?error=waitlist_failed", req.url), 302);
447
+
return Response.redirect(
448
+
new URL("/waitlist?error=waitlist_failed", req.url),
449
+
302
450
+
);
330
451
}
331
452
}
332
453
+7
-7
frontend/src/features/dashboard/handlers.tsx
+7
-7
frontend/src/features/dashboard/handlers.tsx
···
10
10
11
11
async function handleProfilePage(
12
12
req: Request,
13
-
params?: URLPatternResult,
13
+
params?: URLPatternResult
14
14
): Promise<Response> {
15
15
const context = await withAuth(req);
16
16
···
48
48
slices={slices}
49
49
currentUser={context.currentUser}
50
50
profile={profile}
51
-
/>,
51
+
/>
52
52
);
53
53
}
54
54
···
60
60
const authInfo = await atprotoClient.oauth?.getAuthenticationInfo();
61
61
if (!authInfo?.isAuthenticated) {
62
62
return renderHTML(
63
-
<CreateSliceDialog error="Session expired. Please log in again." />,
63
+
<CreateSliceDialog error="Session expired. Please log in again." />
64
64
);
65
65
}
66
66
···
75
75
error="Slice name is required"
76
76
name={name}
77
77
domain={domain}
78
-
/>,
78
+
/>
79
79
);
80
80
}
81
81
···
85
85
error="Primary domain is required"
86
86
name={name}
87
87
domain={domain}
88
-
/>,
88
+
/>
89
89
);
90
90
}
91
91
···
97
97
};
98
98
99
99
const result = await atprotoClient.network.slices.slice.createRecord(
100
-
recordData,
100
+
recordData
101
101
);
102
102
103
103
const uriParts = result.uri.split("/");
···
117
117
error="Failed to create slice record. Please try again."
118
118
name={name}
119
119
domain={domain}
120
-
/>,
120
+
/>
121
121
);
122
122
}
123
123
} catch (_error) {
+1
-1
frontend/src/features/docs/templates/DocsIndexPage.tsx
+1
-1
frontend/src/features/docs/templates/DocsIndexPage.tsx
+2
-6
frontend/src/features/landing/templates/fragments/WaitlistFormModal.tsx
+2
-6
frontend/src/features/landing/templates/fragments/WaitlistFormModal.tsx
···
8
8
<h2 class="font-mono text-xl font-bold text-gray-800 mb-4">
9
9
Join the Waitlist
10
10
</h2>
11
-
<form
12
-
action="/auth/waitlist/initiate"
13
-
method="POST"
14
-
class="space-y-4"
15
-
>
11
+
<form action="/auth/waitlist/initiate" method="POST" class="space-y-4">
16
12
<div>
17
13
<label
18
14
for="handle-input"
···
24
20
type="text"
25
21
id="handle-input"
26
22
name="handle"
27
-
placeholder="alice.bsky.social"
23
+
placeholder="user.bsky.social"
28
24
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-800 font-mono text-sm"
29
25
required
30
26
/>
+1
-1
frontend/src/features/settings/templates/fragments/SettingsForm.tsx
+1
-1
frontend/src/features/settings/templates/fragments/SettingsForm.tsx
+2
-2
frontend/src/features/slices/codegen/templates/SliceCodegenPage.tsx
+2
-2
frontend/src/features/slices/codegen/templates/SliceCodegenPage.tsx
···
44
44
hasSliceAccess={hasSliceAccess}
45
45
title={`${slice.name} - Code Generation`}
46
46
>
47
-
<Card padding="none">
47
+
<Card>
48
48
<Card.Header
49
49
title="TypeScript Client"
50
50
action={
···
64
64
<Card.Content>
65
65
{error ? (
66
66
<div className="p-6">
67
-
<Card variant="danger">
67
+
<Card padding="md" variant="danger">
68
68
<Text as="h3" size="lg" className="font-semibold mb-2">
69
69
❌ Generation Failed
70
70
</Text>
+1
-1
frontend/src/features/slices/lexicon/templates/LexiconDetailPage.tsx
+1
-1
frontend/src/features/slices/lexicon/templates/LexiconDetailPage.tsx
+1
-1
frontend/src/features/slices/lexicon/templates/SliceLexiconPage.tsx
+1
-1
frontend/src/features/slices/lexicon/templates/SliceLexiconPage.tsx
+3
frontend/src/features/slices/mod.ts
+3
frontend/src/features/slices/mod.ts
···
9
9
import { syncRoutes } from "./sync/handlers.tsx";
10
10
import { syncLogsRoutes } from "./sync-logs/handlers.tsx";
11
11
import { jetstreamRoutes } from "./jetstream/handlers.tsx";
12
+
import { waitlistRoutes } from "./waitlist/handlers.tsx";
12
13
13
14
// Export individual route groups
14
15
export {
···
22
23
settingsRoutes,
23
24
syncLogsRoutes,
24
25
syncRoutes,
26
+
waitlistRoutes,
25
27
};
26
28
27
29
// Export consolidated routes array for easy import
···
36
38
...syncRoutes,
37
39
...syncLogsRoutes,
38
40
...jetstreamRoutes,
41
+
...waitlistRoutes,
39
42
];
+1
-1
frontend/src/features/slices/oauth/templates/SliceOAuthPage.tsx
+1
-1
frontend/src/features/slices/oauth/templates/SliceOAuthPage.tsx
+7
-7
frontend/src/features/slices/overview/templates/SliceOverview.tsx
+7
-7
frontend/src/features/slices/overview/templates/SliceOverview.tsx
···
73
73
</div>
74
74
75
75
{(slice.indexedRecordCount ?? 0) > 0 && (
76
-
<Card className="mb-8">
76
+
<Card padding="md" className="mb-8">
77
77
<Text as="h2" size="xl" className="font-semibold mb-4">
78
78
📊 Database Status
79
79
</Text>
···
107
107
)}
108
108
109
109
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
110
-
<Card>
110
+
<Card padding="md">
111
111
<Text as="h2" size="xl" className="font-semibold mb-4 block">
112
112
📚 Lexicon Definitions
113
113
</Text>
···
122
122
</Button>
123
123
</Card>
124
124
125
-
<Card>
125
+
<Card padding="md">
126
126
<Text as="h2" size="xl" className="font-semibold mb-4 block">
127
127
📝 View Records
128
128
</Text>
···
143
143
)}
144
144
</Card>
145
145
146
-
<Card>
146
+
<Card padding="md">
147
147
<Text as="h2" size="xl" className="font-semibold mb-4 block">
148
148
⚡ Code Generation
149
149
</Text>
···
158
158
</Button>
159
159
</Card>
160
160
161
-
<Card>
161
+
<Card padding="md">
162
162
<Text as="h2" size="xl" className="font-semibold mb-4 block">
163
163
📖 API Documentation
164
164
</Text>
···
174
174
</Card>
175
175
176
176
{hasSliceAccess && (
177
-
<Card>
177
+
<Card padding="md">
178
178
<Text as="h2" size="xl" className="font-semibold mb-4 block">
179
179
🔄 Sync
180
180
</Text>
···
191
191
)}
192
192
193
193
{collections.length > 0 && (
194
-
<Card>
194
+
<Card padding="md">
195
195
<Text as="h2" size="xl" className="font-semibold mb-4 block">
196
196
📊 Synced Collections
197
197
</Text>
+43
-12
frontend/src/features/slices/records/templates/fragments/RecordsList.tsx
+43
-12
frontend/src/features/slices/records/templates/fragments/RecordsList.tsx
···
1
-
import type { IndexedRecord } from "../../../../../client.ts";
1
+
import { IndexedRecord } from "@slices/client";
2
2
import { Card } from "../../../../../shared/fragments/Card.tsx";
3
3
import { Text } from "../../../../../shared/fragments/Text.tsx";
4
4
···
12
12
13
13
export function RecordsList({ records }: RecordsListProps) {
14
14
return (
15
-
<Card padding="none">
16
-
<Card.Header
17
-
title={`Records (${records.length})`}
18
-
/>
15
+
<Card>
16
+
<Card.Header title={`Records (${records.length})`} />
19
17
<Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700">
20
18
{records.map((record) => (
21
19
<div key={record.uri} className="p-6">
···
26
24
</Text>
27
25
<dl className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm">
28
26
<div className="grid grid-cols-3 gap-4">
29
-
<Text as="dt" size="sm" variant="muted" className="font-medium">URI:</Text>
27
+
<Text
28
+
as="dt"
29
+
size="sm"
30
+
variant="muted"
31
+
className="font-medium"
32
+
>
33
+
URI:
34
+
</Text>
30
35
<Text as="dd" size="sm" className="col-span-2 break-all">
31
36
{record.uri}
32
37
</Text>
33
38
</div>
34
39
<div className="grid grid-cols-3 gap-4">
35
-
<Text as="dt" size="sm" variant="muted" className="font-medium">
40
+
<Text
41
+
as="dt"
42
+
size="sm"
43
+
variant="muted"
44
+
className="font-medium"
45
+
>
36
46
Collection:
37
47
</Text>
38
48
<Text as="dd" size="sm" className="col-span-2">
···
40
50
</Text>
41
51
</div>
42
52
<div className="grid grid-cols-3 gap-4">
43
-
<Text as="dt" size="sm" variant="muted" className="font-medium">DID:</Text>
53
+
<Text
54
+
as="dt"
55
+
size="sm"
56
+
variant="muted"
57
+
className="font-medium"
58
+
>
59
+
DID:
60
+
</Text>
44
61
<Text as="dd" size="sm" className="col-span-2 break-all">
45
62
{record.did}
46
63
</Text>
47
64
</div>
48
65
<div className="grid grid-cols-3 gap-4">
49
-
<Text as="dt" size="sm" variant="muted" className="font-medium">CID:</Text>
66
+
<Text
67
+
as="dt"
68
+
size="sm"
69
+
variant="muted"
70
+
className="font-medium"
71
+
>
72
+
CID:
73
+
</Text>
50
74
<Text as="dd" size="sm" className="col-span-2 break-all">
51
75
{record.cid}
52
76
</Text>
53
77
</div>
54
78
<div className="grid grid-cols-3 gap-4">
55
-
<Text as="dt" size="sm" variant="muted" className="font-medium">
79
+
<Text
80
+
as="dt"
81
+
size="sm"
82
+
variant="muted"
83
+
className="font-medium"
84
+
>
56
85
Indexed:
57
86
</Text>
58
87
<Text as="dd" size="sm" className="col-span-2">
···
66
95
Record Data
67
96
</Text>
68
97
<pre className="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 p-3 text-xs overflow-auto max-h-64">
69
-
<Text as="span" size="xs">{record.pretty_value ||
70
-
JSON.stringify(record.value, null, 2)}</Text>
98
+
<Text as="span" size="xs">
99
+
{record.pretty_value ||
100
+
JSON.stringify(record.value, null, 2)}
101
+
</Text>
71
102
</pre>
72
103
</div>
73
104
</div>
+2
-2
frontend/src/features/slices/settings/templates/SliceSettings.tsx
+2
-2
frontend/src/features/slices/settings/templates/SliceSettings.tsx
···
56
56
{/* Settings Content */}
57
57
<div className="space-y-8">
58
58
{/* Edit Slice Settings */}
59
-
<Card>
59
+
<Card padding="md">
60
60
<Text as="h2" size="xl" className="font-semibold mb-4">
61
61
Edit Slice Settings
62
62
</Text>
···
104
104
</Card>
105
105
106
106
{/* Danger Zone */}
107
-
<Card className="border-l-4 border-l-red-500">
107
+
<Card padding="md" className="border-l-4 border-l-red-500">
108
108
<Text
109
109
as="h2"
110
110
size="xl"
+1
-1
frontend/src/features/slices/sync/templates/SliceSyncPage.tsx
+1
-1
frontend/src/features/slices/sync/templates/SliceSyncPage.tsx
+234
frontend/src/features/slices/waitlist/api.ts
+234
frontend/src/features/slices/waitlist/api.ts
···
1
+
import type {
2
+
NetworkSlicesWaitlistDefsRequestView,
3
+
NetworkSlicesWaitlistDefsInviteView,
4
+
NetworkSlicesWaitlistRequest,
5
+
NetworkSlicesWaitlistInvite,
6
+
AppBskyActorDefsProfileViewBasic,
7
+
AppBskyActorProfile,
8
+
AtProtoClient,
9
+
} from "../../../client.ts";
10
+
import type { RecordResponse } from "@slices/client";
11
+
import { recordBlobToCdnUrl } from "@slices/client";
12
+
13
+
/**
14
+
* Converts a profile record to ProfileViewBasic format
15
+
*/
16
+
function profileToView(
17
+
record: RecordResponse<AppBskyActorProfile>,
18
+
did: string,
19
+
handle?: string
20
+
): AppBskyActorDefsProfileViewBasic {
21
+
return {
22
+
did,
23
+
handle: handle || did, // Use provided handle or fall back to DID
24
+
displayName: record.value.displayName,
25
+
avatar: record.value.avatar
26
+
? recordBlobToCdnUrl(record, record.value.avatar, "avatar")
27
+
: undefined,
28
+
};
29
+
}
30
+
31
+
/**
32
+
* Converts a waitlist request record to RequestView format
33
+
*/
34
+
function requestToView(
35
+
record: RecordResponse<NetworkSlicesWaitlistRequest>,
36
+
profile?: AppBskyActorDefsProfileViewBasic
37
+
): NetworkSlicesWaitlistDefsRequestView {
38
+
return {
39
+
slice: record.value.slice,
40
+
createdAt: record.value.createdAt,
41
+
profile,
42
+
};
43
+
}
44
+
45
+
/**
46
+
* Converts a waitlist invite record to InviteView format
47
+
*/
48
+
function inviteToView(
49
+
record: RecordResponse<NetworkSlicesWaitlistInvite>,
50
+
profile?: AppBskyActorDefsProfileViewBasic
51
+
): NetworkSlicesWaitlistDefsInviteView {
52
+
return {
53
+
did: record.value.did,
54
+
slice: record.value.slice,
55
+
createdAt: record.value.createdAt,
56
+
expiresAt: record.value.expiresAt,
57
+
uri: record.uri,
58
+
profile,
59
+
};
60
+
}
61
+
62
+
export async function getHydratedWaitlistRequests(
63
+
client: AtProtoClient,
64
+
sliceUri: string
65
+
): Promise<NetworkSlicesWaitlistDefsRequestView[]> {
66
+
// Fetch waitlist requests
67
+
const requestsResponse =
68
+
await client.network.slices.waitlist.request.getRecords({
69
+
where: {
70
+
slice: { eq: sliceUri },
71
+
},
72
+
sortBy: [{ field: "createdAt", direction: "desc" }],
73
+
});
74
+
75
+
if (!requestsResponse.records || requestsResponse.records.length === 0) {
76
+
return [];
77
+
}
78
+
79
+
// Get unique DIDs from requests
80
+
const dids = [...new Set(requestsResponse.records.map((r) => r.did))];
81
+
82
+
// Fetch profiles and actors for all DIDs
83
+
const profilesMap = new Map<string, AppBskyActorDefsProfileViewBasic>();
84
+
85
+
try {
86
+
// Fetch actors to get handles
87
+
const actorsResponse = await client.network.slices.slice.getActors({
88
+
where: {
89
+
did: { in: dids }
90
+
}
91
+
});
92
+
93
+
// Create a map of DIDs to handles
94
+
const handleMap = new Map<string, string>();
95
+
actorsResponse.actors?.forEach((actor) => {
96
+
if (actor.handle) {
97
+
handleMap.set(actor.did, actor.handle);
98
+
}
99
+
});
100
+
101
+
// Fetch Bluesky profiles
102
+
const profileResponses = await Promise.all(
103
+
dids.map((did) =>
104
+
client.app.bsky.actor.profile
105
+
.getRecords({
106
+
where: { did: { eq: did } },
107
+
limit: 1,
108
+
})
109
+
.catch(() => null)
110
+
)
111
+
);
112
+
113
+
profileResponses.forEach((response, index) => {
114
+
if (response?.records?.[0]) {
115
+
const record = response.records[0];
116
+
const did = dids[index];
117
+
const handle = handleMap.get(did);
118
+
profilesMap.set(did, profileToView(record, did, handle));
119
+
}
120
+
});
121
+
} catch (error) {
122
+
console.error("Error fetching profiles:", error);
123
+
}
124
+
125
+
// Transform to RequestView format with profiles
126
+
return requestsResponse.records.map((record) =>
127
+
requestToView(record, profilesMap.get(record.did))
128
+
);
129
+
}
130
+
131
+
export async function getHydratedWaitlistInvites(
132
+
client: AtProtoClient,
133
+
sliceUri: string
134
+
): Promise<NetworkSlicesWaitlistDefsInviteView[]> {
135
+
// Fetch waitlist invites
136
+
const invitesResponse =
137
+
await client.network.slices.waitlist.invite.getRecords({
138
+
where: {
139
+
slice: { eq: sliceUri },
140
+
},
141
+
sortBy: [{ field: "createdAt", direction: "desc" }],
142
+
});
143
+
144
+
if (!invitesResponse.records || invitesResponse.records.length === 0) {
145
+
return [];
146
+
}
147
+
148
+
// Get unique DIDs from invites
149
+
const dids = [...new Set(invitesResponse.records.map((r) => r.value.did))];
150
+
151
+
// Fetch profiles and actors for all DIDs
152
+
const profilesMap = new Map<string, AppBskyActorDefsProfileViewBasic>();
153
+
154
+
try {
155
+
// Fetch actors to get handles
156
+
const actorsResponse = await client.network.slices.slice.getActors({
157
+
where: {
158
+
did: { in: dids }
159
+
}
160
+
});
161
+
162
+
// Create a map of DIDs to handles
163
+
const handleMap = new Map<string, string>();
164
+
actorsResponse.actors?.forEach((actor) => {
165
+
if (actor.handle) {
166
+
handleMap.set(actor.did, actor.handle);
167
+
}
168
+
});
169
+
170
+
// Fetch Bluesky profiles
171
+
const profileResponses = await Promise.all(
172
+
dids.map((did) =>
173
+
client.app.bsky.actor.profile
174
+
.getRecords({
175
+
where: { did: { eq: did } },
176
+
limit: 1,
177
+
})
178
+
.catch(() => null)
179
+
)
180
+
);
181
+
182
+
profileResponses.forEach((response, index) => {
183
+
if (response?.records?.[0]) {
184
+
const record = response.records[0];
185
+
const did = dids[index];
186
+
const handle = handleMap.get(did);
187
+
profilesMap.set(did, profileToView(record, did, handle));
188
+
}
189
+
});
190
+
} catch (error) {
191
+
console.error("Error fetching profiles:", error);
192
+
}
193
+
194
+
// Transform to InviteView format with profiles
195
+
return invitesResponse.records.map((record) =>
196
+
inviteToView(record, profilesMap.get(record.value.did))
197
+
);
198
+
}
199
+
200
+
export async function createInviteFromRequest(
201
+
client: AtProtoClient,
202
+
requestUri: string,
203
+
requestDid: string,
204
+
sliceUri: string
205
+
): Promise<{ uri: string; cid: string }> {
206
+
// Create the invite record
207
+
const result = await client.network.slices.waitlist.invite.createRecord({
208
+
did: requestDid,
209
+
slice: sliceUri,
210
+
createdAt: new Date().toISOString(),
211
+
});
212
+
213
+
// Delete the request record
214
+
try {
215
+
const rkey = requestUri.split("/").pop();
216
+
if (rkey) {
217
+
await client.network.slices.waitlist.request.deleteRecord(rkey);
218
+
}
219
+
} catch (error) {
220
+
console.error("Failed to delete request after creating invite:", error);
221
+
}
222
+
223
+
return result;
224
+
}
225
+
226
+
export async function removeInvite(
227
+
client: AtProtoClient,
228
+
inviteUri: string
229
+
): Promise<void> {
230
+
const rkey = inviteUri.split("/").pop();
231
+
if (rkey) {
232
+
await client.network.slices.waitlist.invite.deleteRecord(rkey);
233
+
}
234
+
}
+347
frontend/src/features/slices/waitlist/handlers.tsx
+347
frontend/src/features/slices/waitlist/handlers.tsx
···
1
+
import type { Route } from "@std/http/unstable-route";
2
+
import { withAuth } from "../../../routes/middleware.ts";
3
+
import { withSliceAccess } from "../../../routes/slice-middleware.ts";
4
+
import { extractSliceParams, buildSliceUrlFromView } from "../../../utils/slice-params.ts";
5
+
import { getSliceClient } from "../../../utils/client.ts";
6
+
import { getRkeyFromUri, buildSliceUri } from "../../../utils/at-uri.ts";
7
+
import { renderHTML } from "../../../utils/render.tsx";
8
+
import { hxRedirect } from "../../../utils/htmx.ts";
9
+
import { SliceWaitlistPage } from "./templates/SliceWaitlistPage.tsx";
10
+
import { CreateInviteModal } from "./templates/fragments/CreateInviteModal.tsx";
11
+
import type {
12
+
NetworkSlicesWaitlistDefsRequestView,
13
+
NetworkSlicesWaitlistDefsInviteView,
14
+
NetworkSlicesWaitlistInvite,
15
+
} from "../../../client.ts";
16
+
import {
17
+
getHydratedWaitlistRequests,
18
+
getHydratedWaitlistInvites,
19
+
} from "./api.ts";
20
+
21
+
async function handleSliceWaitlistPage(
22
+
req: Request,
23
+
params?: URLPatternResult
24
+
): Promise<Response> {
25
+
const authContext = await withAuth(req);
26
+
const sliceParams = extractSliceParams(params);
27
+
28
+
if (!sliceParams) {
29
+
return Response.redirect(new URL("/", req.url), 302);
30
+
}
31
+
32
+
const context = await withSliceAccess(
33
+
authContext,
34
+
sliceParams.handle,
35
+
sliceParams.sliceId
36
+
);
37
+
38
+
// Check if slice exists and user has access
39
+
if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) {
40
+
return new Response("Slice not found or access denied", { status: 404 });
41
+
}
42
+
43
+
// Get the active tab from query params
44
+
const url = new URL(req.url);
45
+
const activeTab = url.searchParams.get("tab") || "requests";
46
+
47
+
// Fetch waitlist requests and invites from the API
48
+
const sliceClient = getSliceClient(
49
+
authContext,
50
+
sliceParams.sliceId,
51
+
context.sliceContext.profileDid
52
+
);
53
+
54
+
// Build the slice URI to filter by
55
+
const sliceUri = buildSliceUri(
56
+
context.sliceContext.profileDid,
57
+
sliceParams.sliceId
58
+
);
59
+
60
+
let requests: NetworkSlicesWaitlistDefsRequestView[] = [];
61
+
let invites: NetworkSlicesWaitlistDefsInviteView[] = [];
62
+
63
+
try {
64
+
// Fetch hydrated requests with profile information
65
+
requests = await getHydratedWaitlistRequests(sliceClient, sliceUri);
66
+
67
+
// Fetch hydrated invites with profile information
68
+
invites = await getHydratedWaitlistInvites(sliceClient, sliceUri);
69
+
} catch (error) {
70
+
console.error("Error fetching waitlist data:", error);
71
+
// Continue with empty arrays if fetch fails
72
+
}
73
+
74
+
return renderHTML(
75
+
<SliceWaitlistPage
76
+
slice={context.sliceContext!.slice!}
77
+
sliceId={sliceParams.sliceId}
78
+
requests={requests}
79
+
invites={invites}
80
+
currentUser={authContext.currentUser}
81
+
hasSliceAccess={context.sliceContext?.hasAccess}
82
+
activeTab={activeTab}
83
+
/>
84
+
);
85
+
}
86
+
87
+
async function handleCreateInviteModal(
88
+
req: Request,
89
+
params?: URLPatternResult
90
+
): Promise<Response> {
91
+
const authContext = await withAuth(req);
92
+
const sliceParams = extractSliceParams(params);
93
+
94
+
if (!sliceParams) {
95
+
return new Response("Invalid slice parameters", { status: 400 });
96
+
}
97
+
98
+
const context = await withSliceAccess(
99
+
authContext,
100
+
sliceParams.handle,
101
+
sliceParams.sliceId
102
+
);
103
+
104
+
if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) {
105
+
return new Response("Slice not found or access denied", { status: 404 });
106
+
}
107
+
108
+
return renderHTML(
109
+
<CreateInviteModal
110
+
slice={context.sliceContext!.slice!}
111
+
sliceId={sliceParams.sliceId}
112
+
/>
113
+
);
114
+
}
115
+
116
+
async function handleCreateInvite(
117
+
req: Request,
118
+
params?: URLPatternResult
119
+
): Promise<Response> {
120
+
const authContext = await withAuth(req);
121
+
const sliceParams = extractSliceParams(params);
122
+
123
+
if (!sliceParams) {
124
+
return new Response("Invalid slice parameters", { status: 400 });
125
+
}
126
+
127
+
const context = await withSliceAccess(
128
+
authContext,
129
+
sliceParams.handle,
130
+
sliceParams.sliceId
131
+
);
132
+
133
+
if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) {
134
+
return new Response("Slice not found or access denied", { status: 404 });
135
+
}
136
+
137
+
const formData = await req.formData();
138
+
const did = formData.get("did")?.toString()?.trim();
139
+
const expiresAt = formData.get("expiresAt")?.toString();
140
+
141
+
if (!did) {
142
+
return new Response("DID is required", { status: 400 });
143
+
}
144
+
145
+
try {
146
+
const sliceClient = getSliceClient(
147
+
authContext,
148
+
sliceParams.sliceId,
149
+
context.sliceContext.profileDid
150
+
);
151
+
152
+
const sliceUri = buildSliceUri(
153
+
context.sliceContext.profileDid,
154
+
sliceParams.sliceId
155
+
);
156
+
157
+
const inviteData: NetworkSlicesWaitlistInvite = {
158
+
did,
159
+
slice: sliceUri,
160
+
createdAt: new Date().toISOString(),
161
+
};
162
+
163
+
// Convert datetime-local input to ISO 8601 string
164
+
if (expiresAt) {
165
+
// datetime-local gives us "2024-12-25T14:30" format
166
+
// We need to convert it to a proper ISO string with timezone
167
+
const expiresDate = new Date(expiresAt);
168
+
if (!isNaN(expiresDate.getTime())) {
169
+
inviteData.expiresAt = expiresDate.toISOString();
170
+
}
171
+
}
172
+
173
+
await sliceClient.network.slices.waitlist.invite.createRecord(inviteData);
174
+
175
+
// Redirect back to the waitlist page with invites tab
176
+
const redirectUrl = buildSliceUrlFromView(
177
+
context.sliceContext!.slice!,
178
+
sliceParams.sliceId,
179
+
"waitlist?tab=invites"
180
+
);
181
+
return hxRedirect(redirectUrl);
182
+
} catch (error) {
183
+
// Use the raw error message from the client
184
+
const userErrorMessage =
185
+
error instanceof Error ? error.message : "Failed to create invite";
186
+
187
+
// Return the error modal with the specific error message
188
+
return renderHTML(
189
+
<CreateInviteModal
190
+
slice={context.sliceContext!.slice!}
191
+
sliceId={sliceParams.sliceId}
192
+
error={userErrorMessage}
193
+
/>
194
+
);
195
+
}
196
+
}
197
+
198
+
async function handleRevokeInvite(
199
+
req: Request,
200
+
params?: URLPatternResult
201
+
): Promise<Response> {
202
+
const authContext = await withAuth(req);
203
+
const sliceParams = extractSliceParams(params);
204
+
205
+
if (!sliceParams) {
206
+
return new Response("Invalid slice parameters", { status: 400 });
207
+
}
208
+
209
+
const context = await withSliceAccess(
210
+
authContext,
211
+
sliceParams.handle,
212
+
sliceParams.sliceId
213
+
);
214
+
215
+
if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) {
216
+
return new Response("Slice not found or access denied", { status: 404 });
217
+
}
218
+
219
+
const formData = await req.formData();
220
+
const uri = formData.get("uri")?.toString();
221
+
222
+
if (!uri) {
223
+
return new Response("URI is required", { status: 400 });
224
+
}
225
+
226
+
try {
227
+
const sliceClient = getSliceClient(
228
+
authContext,
229
+
sliceParams.sliceId,
230
+
context.sliceContext.profileDid
231
+
);
232
+
233
+
const rkey = getRkeyFromUri(uri);
234
+
await sliceClient.network.slices.waitlist.invite.deleteRecord(rkey);
235
+
236
+
// Return success with HX-Refresh to reload the page
237
+
return new Response("", {
238
+
status: 200,
239
+
headers: {
240
+
"HX-Refresh": "true",
241
+
},
242
+
});
243
+
} catch (error) {
244
+
console.error("Error revoking invite:", error);
245
+
return new Response("Failed to revoke invite", { status: 500 });
246
+
}
247
+
}
248
+
249
+
async function handleCreateInviteFromRequest(
250
+
req: Request,
251
+
params?: URLPatternResult
252
+
): Promise<Response> {
253
+
const authContext = await withAuth(req);
254
+
const sliceParams = extractSliceParams(params);
255
+
256
+
if (!sliceParams) {
257
+
return new Response("Invalid slice parameters", { status: 400 });
258
+
}
259
+
260
+
const context = await withSliceAccess(
261
+
authContext,
262
+
sliceParams.handle,
263
+
sliceParams.sliceId
264
+
);
265
+
266
+
if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) {
267
+
return new Response("Slice not found or access denied", { status: 404 });
268
+
}
269
+
270
+
try {
271
+
const sliceClient = getSliceClient(
272
+
authContext,
273
+
sliceParams.sliceId,
274
+
context.sliceContext.profileDid
275
+
);
276
+
277
+
const formData = await req.formData();
278
+
const did = formData.get("did")?.toString()?.trim();
279
+
280
+
console.log("Creating invite for DID:", did);
281
+
282
+
if (!did) {
283
+
return new Response("DID is required", { status: 400 });
284
+
}
285
+
286
+
const sliceUri = buildSliceUri(context.sliceContext.profileDid, sliceParams.sliceId);
287
+
288
+
const inviteData: NetworkSlicesWaitlistInvite = {
289
+
did,
290
+
slice: sliceUri,
291
+
createdAt: new Date().toISOString(),
292
+
};
293
+
294
+
await sliceClient.network.slices.waitlist.invite.createRecord(inviteData);
295
+
296
+
// Return success with HX-Refresh to reload the page
297
+
return new Response("", {
298
+
status: 200,
299
+
headers: {
300
+
"HX-Refresh": "true",
301
+
},
302
+
});
303
+
} catch (error) {
304
+
console.error("Error creating invite:", error);
305
+
const userErrorMessage =
306
+
error instanceof Error ? error.message : "Failed to create invite";
307
+
return new Response(userErrorMessage, { status: 500 });
308
+
}
309
+
}
310
+
311
+
export const waitlistRoutes: Route[] = [
312
+
{
313
+
method: "GET",
314
+
pattern: new URLPattern({
315
+
pathname: "/profile/:handle/slice/:rkey/waitlist",
316
+
}),
317
+
handler: handleSliceWaitlistPage,
318
+
},
319
+
{
320
+
method: "GET",
321
+
pattern: new URLPattern({
322
+
pathname: "/profile/:handle/slice/:rkey/waitlist/invite/new",
323
+
}),
324
+
handler: handleCreateInviteModal,
325
+
},
326
+
{
327
+
method: "POST",
328
+
pattern: new URLPattern({
329
+
pathname: "/profile/:handle/slice/:rkey/waitlist/invite",
330
+
}),
331
+
handler: handleCreateInvite,
332
+
},
333
+
{
334
+
method: "POST",
335
+
pattern: new URLPattern({
336
+
pathname: "/profile/:handle/slice/:rkey/waitlist/invite/from-request",
337
+
}),
338
+
handler: handleCreateInviteFromRequest,
339
+
},
340
+
{
341
+
method: "DELETE",
342
+
pattern: new URLPattern({
343
+
pathname: "/profile/:handle/slice/:rkey/waitlist/invite",
344
+
}),
345
+
handler: handleRevokeInvite,
346
+
},
347
+
];
+94
frontend/src/features/slices/waitlist/templates/SliceWaitlistPage.tsx
+94
frontend/src/features/slices/waitlist/templates/SliceWaitlistPage.tsx
···
1
+
import { SlicePage } from "../../shared/fragments/SlicePage.tsx";
2
+
import { WaitlistRequestsList } from "./fragments/WaitlistRequestsList.tsx";
3
+
import { WaitlistInvitesList } from "./fragments/WaitlistInvitesList.tsx";
4
+
import { Button } from "../../../../shared/fragments/Button.tsx";
5
+
import { Tabs } from "../../../../shared/fragments/Tabs.tsx";
6
+
import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts";
7
+
import type { AuthenticatedUser } from "../../../../routes/middleware.ts";
8
+
import type {
9
+
NetworkSlicesSliceDefsSliceView,
10
+
NetworkSlicesWaitlistDefsRequestView,
11
+
NetworkSlicesWaitlistDefsInviteView
12
+
} from "../../../../client.ts";
13
+
14
+
interface SliceWaitlistPageProps {
15
+
slice: NetworkSlicesSliceDefsSliceView;
16
+
sliceId: string;
17
+
requests?: NetworkSlicesWaitlistDefsRequestView[];
18
+
invites?: NetworkSlicesWaitlistDefsInviteView[];
19
+
currentUser?: AuthenticatedUser;
20
+
hasSliceAccess?: boolean;
21
+
activeTab?: string;
22
+
}
23
+
24
+
export function SliceWaitlistPage({
25
+
slice,
26
+
sliceId,
27
+
requests = [],
28
+
invites = [],
29
+
currentUser,
30
+
hasSliceAccess,
31
+
activeTab = "requests",
32
+
}: SliceWaitlistPageProps) {
33
+
return (
34
+
<SlicePage
35
+
slice={slice}
36
+
sliceId={sliceId}
37
+
currentTab="waitlist"
38
+
currentUser={currentUser}
39
+
hasSliceAccess={hasSliceAccess}
40
+
title={`${slice.name} - Waitlist`}
41
+
>
42
+
<div className="space-y-6">
43
+
<div className="flex justify-end">
44
+
<Button
45
+
variant="primary"
46
+
size="md"
47
+
hx-get={buildSliceUrlFromView(slice, sliceId, "waitlist/invite/new")}
48
+
hx-target="#modal-container"
49
+
>
50
+
Create Invite
51
+
</Button>
52
+
</div>
53
+
54
+
<Tabs variant="bordered">
55
+
<Tabs.List>
56
+
<Tabs.Tab
57
+
active={activeTab === "requests"}
58
+
count={requests.length}
59
+
hxGet={buildSliceUrlFromView(slice, sliceId, "waitlist?tab=requests")}
60
+
hxTarget="body"
61
+
>
62
+
Requests
63
+
</Tabs.Tab>
64
+
<Tabs.Tab
65
+
active={activeTab === "invites"}
66
+
count={invites.length}
67
+
hxGet={buildSliceUrlFromView(slice, sliceId, "waitlist?tab=invites")}
68
+
hxTarget="body"
69
+
>
70
+
Invites
71
+
</Tabs.Tab>
72
+
</Tabs.List>
73
+
74
+
<Tabs.Content active={activeTab === "requests"}>
75
+
<WaitlistRequestsList
76
+
requests={requests}
77
+
invites={invites}
78
+
slice={slice}
79
+
sliceId={sliceId}
80
+
/>
81
+
</Tabs.Content>
82
+
83
+
<Tabs.Content active={activeTab === "invites"}>
84
+
<WaitlistInvitesList
85
+
invites={invites}
86
+
slice={slice}
87
+
sliceId={sliceId}
88
+
/>
89
+
</Tabs.Content>
90
+
</Tabs>
91
+
</div>
92
+
</SlicePage>
93
+
);
94
+
}
+73
frontend/src/features/slices/waitlist/templates/fragments/CreateInviteModal.tsx
+73
frontend/src/features/slices/waitlist/templates/fragments/CreateInviteModal.tsx
···
1
+
import { Modal } from "../../../../../shared/fragments/Modal.tsx";
2
+
import { Input } from "../../../../../shared/fragments/Input.tsx";
3
+
import { Button } from "../../../../../shared/fragments/Button.tsx";
4
+
import { Text } from "../../../../../shared/fragments/Text.tsx";
5
+
import { buildSliceUrlFromView } from "../../../../../utils/slice-params.ts";
6
+
import type { NetworkSlicesSliceDefsSliceView } from "../../../../../client.ts";
7
+
8
+
interface CreateInviteModalProps {
9
+
slice: NetworkSlicesSliceDefsSliceView;
10
+
sliceId: string;
11
+
error?: string;
12
+
}
13
+
14
+
export function CreateInviteModal({ slice, sliceId, error }: CreateInviteModalProps) {
15
+
return (
16
+
<Modal
17
+
title="Create Invite"
18
+
description="Grant a specific DID access to your slice"
19
+
size="md"
20
+
>
21
+
{error && (
22
+
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
23
+
<Text as="p" variant="error" size="sm">
24
+
{error}
25
+
</Text>
26
+
</div>
27
+
)}
28
+
<form
29
+
hx-post={buildSliceUrlFromView(slice, sliceId, "waitlist/invite")}
30
+
className="space-y-4"
31
+
>
32
+
<div>
33
+
<Text as="label" variant="label" className="block mb-2">
34
+
DID
35
+
</Text>
36
+
<Input
37
+
type="text"
38
+
name="did"
39
+
placeholder="did:plc:example123..."
40
+
required
41
+
className="w-full"
42
+
/>
43
+
<Text as="p" variant="muted" size="sm" className="mt-1">
44
+
The AT Protocol DID to grant access
45
+
</Text>
46
+
</div>
47
+
48
+
<div>
49
+
<Text as="label" variant="label" className="block mb-2">
50
+
Expires At (Optional)
51
+
</Text>
52
+
<Input type="datetime-local" name="expiresAt" className="w-full" />
53
+
<Text as="p" variant="muted" size="sm" className="mt-1">
54
+
Leave empty for no expiration
55
+
</Text>
56
+
</div>
57
+
58
+
<div className="flex justify-end gap-3 pt-4">
59
+
<Button
60
+
type="button"
61
+
variant="outline"
62
+
_="on click set #modal-container's innerHTML to ''"
63
+
>
64
+
Cancel
65
+
</Button>
66
+
<Button type="submit" variant="success">
67
+
Create Invite
68
+
</Button>
69
+
</div>
70
+
</form>
71
+
</Modal>
72
+
);
73
+
}
+93
frontend/src/features/slices/waitlist/templates/fragments/WaitlistInvitesList.tsx
+93
frontend/src/features/slices/waitlist/templates/fragments/WaitlistInvitesList.tsx
···
1
+
import { ListItem } from "../../../../../shared/fragments/ListItem.tsx";
2
+
import { Button } from "../../../../../shared/fragments/Button.tsx";
3
+
import { Text } from "../../../../../shared/fragments/Text.tsx";
4
+
import { EmptyState } from "../../../../../shared/fragments/EmptyState.tsx";
5
+
import { ActorAvatar } from "../../../../../shared/fragments/ActorAvatar.tsx";
6
+
import { buildSliceUrlFromView } from "../../../../../utils/slice-params.ts";
7
+
import { timeAgo } from "../../../../../utils/time.ts";
8
+
import { UserCheck } from "lucide-preact";
9
+
import type {
10
+
NetworkSlicesWaitlistDefsInviteView,
11
+
NetworkSlicesSliceDefsSliceView
12
+
} from "../../../../../client.ts";
13
+
14
+
interface WaitlistInvitesListProps {
15
+
invites: NetworkSlicesWaitlistDefsInviteView[];
16
+
slice: NetworkSlicesSliceDefsSliceView;
17
+
sliceId: string;
18
+
}
19
+
20
+
export function WaitlistInvitesList({
21
+
invites,
22
+
slice,
23
+
sliceId,
24
+
}: WaitlistInvitesListProps) {
25
+
const isExpired = (invite: NetworkSlicesWaitlistDefsInviteView) => {
26
+
if (!invite.expiresAt) return false;
27
+
return new Date(invite.expiresAt) < new Date();
28
+
};
29
+
30
+
if (invites.length === 0) {
31
+
return (
32
+
<EmptyState
33
+
icon={<UserCheck size={48} strokeWidth={1} />}
34
+
title="No invites created"
35
+
description="Create invites to grant specific DIDs access to your slice."
36
+
withPadding
37
+
/>
38
+
);
39
+
}
40
+
41
+
return (
42
+
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-sm">
43
+
{invites.map((invite, index) => (
44
+
<ListItem key={`invite-${index}`}>
45
+
<div className="flex items-center justify-between w-full px-6 py-4">
46
+
<div className="flex items-center gap-3 flex-1 min-w-0">
47
+
<ActorAvatar
48
+
profile={invite.profile || { handle: invite.did }}
49
+
size={40}
50
+
/>
51
+
<div className="flex-1 min-w-0">
52
+
<Text as="div" size="sm" className="font-medium truncate">
53
+
{invite.profile?.displayName || invite.profile?.handle || invite.did}
54
+
</Text>
55
+
<div className="flex items-center gap-4 mt-0.5">
56
+
<Text as="div" size="xs" variant="muted">
57
+
{invite.profile?.handle && invite.profile.handle !== invite.did && `@${invite.profile.handle} • `}Created {timeAgo(invite.createdAt)}
58
+
</Text>
59
+
{invite.expiresAt && (
60
+
<Text
61
+
as="div"
62
+
size="xs"
63
+
variant={isExpired(invite) ? "error" : "muted"}
64
+
>
65
+
{isExpired(invite) ? "Expired" : "Expires"} {timeAgo(invite.expiresAt)}
66
+
</Text>
67
+
)}
68
+
</div>
69
+
</div>
70
+
</div>
71
+
<div className="ml-4">
72
+
<form
73
+
hx-delete={buildSliceUrlFromView(slice, sliceId, "waitlist/invite")}
74
+
hx-trigger="submit"
75
+
hx-confirm="Revoke this invite?"
76
+
style="display: inline;"
77
+
>
78
+
<input type="hidden" name="uri" value={invite.uri} />
79
+
<Button
80
+
type="submit"
81
+
variant="outline"
82
+
size="sm"
83
+
>
84
+
Revoke
85
+
</Button>
86
+
</form>
87
+
</div>
88
+
</div>
89
+
</ListItem>
90
+
))}
91
+
</div>
92
+
);
93
+
}
+92
frontend/src/features/slices/waitlist/templates/fragments/WaitlistRequestsList.tsx
+92
frontend/src/features/slices/waitlist/templates/fragments/WaitlistRequestsList.tsx
···
1
+
import { ListItem } from "../../../../../shared/fragments/ListItem.tsx";
2
+
import { Button } from "../../../../../shared/fragments/Button.tsx";
3
+
import { Text } from "../../../../../shared/fragments/Text.tsx";
4
+
import { EmptyState } from "../../../../../shared/fragments/EmptyState.tsx";
5
+
import { ActorAvatar } from "../../../../../shared/fragments/ActorAvatar.tsx";
6
+
import { Users } from "lucide-preact";
7
+
import type {
8
+
NetworkSlicesWaitlistDefsRequestView,
9
+
NetworkSlicesWaitlistDefsInviteView,
10
+
NetworkSlicesSliceDefsSliceView
11
+
} from "../../../../../client.ts";
12
+
import { buildSliceUrlFromView } from "../../../../../utils/slice-params.ts";
13
+
import { timeAgo } from "../../../../../utils/time.ts";
14
+
15
+
interface WaitlistRequestsListProps {
16
+
requests: NetworkSlicesWaitlistDefsRequestView[];
17
+
invites: NetworkSlicesWaitlistDefsInviteView[];
18
+
slice: NetworkSlicesSliceDefsSliceView;
19
+
sliceId: string;
20
+
}
21
+
22
+
export function WaitlistRequestsList({
23
+
requests,
24
+
invites,
25
+
slice,
26
+
sliceId,
27
+
}: WaitlistRequestsListProps) {
28
+
if (requests.length === 0) {
29
+
return (
30
+
<EmptyState
31
+
icon={<Users size={48} strokeWidth={1} />}
32
+
title="No requests yet"
33
+
description="Waitlist requests will appear here when users request access."
34
+
withPadding
35
+
/>
36
+
);
37
+
}
38
+
39
+
return (
40
+
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-sm">
41
+
{requests.map((request, index) => {
42
+
// Check if this DID already has an invite
43
+
const requestDid = request.profile?.did || "unknown";
44
+
const hasInvite = invites.some(invite => invite.did === requestDid);
45
+
46
+
return (
47
+
<ListItem key={`request-${index}`}>
48
+
<div className="flex items-center justify-between w-full px-6 py-4">
49
+
<div className="flex items-center gap-3 flex-1 min-w-0">
50
+
<ActorAvatar
51
+
profile={request.profile || { handle: requestDid }}
52
+
size={40}
53
+
/>
54
+
<div className="flex-1 min-w-0">
55
+
<Text
56
+
as="div"
57
+
size="sm"
58
+
className="font-medium truncate"
59
+
>
60
+
{request.profile?.displayName || request.profile?.handle || requestDid}
61
+
</Text>
62
+
<Text as="div" size="xs" variant="muted" className="mt-0.5">
63
+
{request.profile?.handle && request.profile.handle !== requestDid && `@${request.profile.handle} • `}Requested {timeAgo(request.createdAt)}
64
+
</Text>
65
+
</div>
66
+
</div>
67
+
<div className="flex items-center gap-2 ml-4">
68
+
{!hasInvite && (
69
+
<Button
70
+
variant="success"
71
+
size="sm"
72
+
hx-post={buildSliceUrlFromView(slice, sliceId, "waitlist/invite/from-request")}
73
+
hx-vals={JSON.stringify({ did: requestDid })}
74
+
hx-target="closest div[class*='bg-white']"
75
+
hx-swap="outerHTML"
76
+
>
77
+
Invite
78
+
</Button>
79
+
)}
80
+
{hasInvite && (
81
+
<Text size="sm" variant="muted">
82
+
Already invited
83
+
</Text>
84
+
)}
85
+
</div>
86
+
</div>
87
+
</ListItem>
88
+
);
89
+
})}
90
+
</div>
91
+
);
92
+
}
+16
frontend/src/features/waitlist/handlers.tsx
+16
frontend/src/features/waitlist/handlers.tsx
···
2
2
import { withAuth } from "../../routes/middleware.ts";
3
3
import { renderHTML } from "../../utils/render.tsx";
4
4
import { WaitlistPage } from "./templates/WaitlistPage.tsx";
5
+
import { publicClient, SLICE_URI } from "../../config.ts";
6
+
import { getHydratedWaitlistRequests } from "../slices/waitlist/api.ts";
5
7
6
8
async function handleWaitlistPage(req: Request): Promise<Response> {
7
9
const context = await withAuth(req);
···
12
14
const handle = url.searchParams.get("handle");
13
15
const error = url.searchParams.get("error");
14
16
17
+
// Fetch recent waitlist requests to show avatars for social proof
18
+
let recentRequests;
19
+
if (SLICE_URI) {
20
+
try {
21
+
recentRequests = await getHydratedWaitlistRequests(publicClient, SLICE_URI);
22
+
// Limit to most recent 50 and reverse to show newest first
23
+
recentRequests = recentRequests.slice(0, 50);
24
+
} catch (error) {
25
+
console.error("Failed to fetch recent waitlist requests:", error);
26
+
// Continue without recent requests if fetch fails
27
+
}
28
+
}
29
+
15
30
return renderHTML(
16
31
<WaitlistPage
17
32
success={success}
18
33
handle={handle || undefined}
19
34
error={error || undefined}
20
35
currentUser={context.currentUser}
36
+
recentRequests={recentRequests}
21
37
/>
22
38
);
23
39
}
+6
-3
frontend/src/features/waitlist/templates/WaitlistPage.tsx
+6
-3
frontend/src/features/waitlist/templates/WaitlistPage.tsx
···
3
3
import { WaitlistSuccess } from "./fragments/WaitlistSuccess.tsx";
4
4
import { Text } from "../../../shared/fragments/Text.tsx";
5
5
import type { AuthenticatedUser } from "../../../routes/middleware.ts";
6
+
import type { NetworkSlicesWaitlistDefsRequestView } from "../../../client.ts";
6
7
7
8
interface WaitlistPageProps {
8
9
success?: boolean;
9
10
handle?: string;
10
11
error?: string;
11
12
currentUser?: AuthenticatedUser;
13
+
recentRequests?: NetworkSlicesWaitlistDefsRequestView[];
12
14
}
13
15
14
16
export function WaitlistPage({
···
16
18
handle,
17
19
error,
18
20
currentUser,
21
+
recentRequests,
19
22
}: WaitlistPageProps) {
20
23
return (
21
24
<Layout title="Join the Waitlist - Slices" currentUser={currentUser}>
22
-
<div className="min-h-screen bg-white dark:bg-zinc-900 flex items-center justify-center px-4 py-16">
25
+
<div className="min-h-[calc(100vh-3.5rem)] bg-white dark:bg-zinc-900 flex items-center justify-center px-4 py-16">
23
26
<div className="w-full max-w-md">
24
27
{success ? (
25
-
<WaitlistSuccess handle={handle} />
28
+
<WaitlistSuccess handle={handle} recentRequests={recentRequests} />
26
29
) : (
27
30
<>
28
31
<div className="text-center mb-8">
···
33
36
Be among the first to experience the future of AT Protocol ecosystem tools.
34
37
</Text>
35
38
</div>
36
-
<WaitlistForm error={error} />
39
+
<WaitlistForm error={error} recentRequests={recentRequests} />
37
40
</>
38
41
)}
39
42
</div>
+44
-6
frontend/src/features/waitlist/templates/fragments/WaitlistForm.tsx
+44
-6
frontend/src/features/waitlist/templates/fragments/WaitlistForm.tsx
···
3
3
import { Card } from "../../../../shared/fragments/Card.tsx";
4
4
import { Text } from "../../../../shared/fragments/Text.tsx";
5
5
import { FlashMessage } from "../../../../shared/fragments/FlashMessage.tsx";
6
+
import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx";
7
+
import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts";
6
8
7
9
interface WaitlistFormProps {
8
10
error?: string;
11
+
recentRequests?: NetworkSlicesWaitlistDefsRequestView[];
9
12
}
10
13
11
-
export function WaitlistForm({ error }: WaitlistFormProps) {
14
+
export function WaitlistForm({ error, recentRequests }: WaitlistFormProps) {
12
15
const getErrorMessage = (error: string) => {
13
16
switch (error) {
14
17
case "oauth_not_configured":
···
19
22
return "Could not retrieve user information.";
20
23
case "waitlist_failed":
21
24
return "Failed to join waitlist. Please try again.";
25
+
case "invite_required":
26
+
return "You need an invite to access this service. Join the waitlist to request access.";
27
+
case "already_on_waitlist":
28
+
return "You're already on the waitlist! We'll notify you when your invite is ready.";
22
29
default:
23
30
return "An error occurred. Please try again.";
24
31
}
25
32
};
26
33
27
34
return (
28
-
<Card>
35
+
<Card padding="md">
36
+
{recentRequests && recentRequests.length > 0 && (
37
+
<div className="mb-6 text-center">
38
+
<Text as="p" size="sm" variant="muted" className="mb-3">
39
+
Join {recentRequests.length} others who are waiting
40
+
</Text>
41
+
<div className="flex flex-wrap justify-center gap-1">
42
+
{recentRequests.slice(0, 20).map((request, index) => (
43
+
<div key={index} className="relative">
44
+
<ActorAvatar
45
+
profile={request.profile || { handle: "user" }}
46
+
size={24}
47
+
className="border border-white dark:border-zinc-800"
48
+
/>
49
+
</div>
50
+
))}
51
+
{recentRequests.length > 20 && (
52
+
<div className="w-6 h-6 bg-zinc-100 dark:bg-zinc-800 border border-white dark:border-zinc-800 rounded-full flex items-center justify-center">
53
+
<Text size="xs" variant="muted">
54
+
+{recentRequests.length - 20}
55
+
</Text>
56
+
</div>
57
+
)}
58
+
</div>
59
+
</div>
60
+
)}
61
+
29
62
<form action="/auth/waitlist/initiate" method="POST">
30
63
<div className="space-y-6">
31
64
{error && (
···
36
69
<Input
37
70
label="Your handle"
38
71
name="handle"
39
-
placeholder="alice.bsky.social"
72
+
placeholder="user.bsky.social"
40
73
required
41
74
/>
42
75
<Text as="p" size="xs" variant="muted">
···
45
78
</div>
46
79
47
80
<div className="space-y-4">
48
-
<Button type="submit" variant="primary" className="w-full justify-center">
81
+
<Button
82
+
type="submit"
83
+
variant="primary"
84
+
className="w-full justify-center"
85
+
>
49
86
Join Waitlist
50
87
</Button>
51
88
52
89
<Text as="p" size="xs" variant="muted" className="text-center">
53
-
By joining the waitlist, you'll be notified when Slices is ready for you.
90
+
By joining the waitlist, you'll be notified when Slices is ready
91
+
for you.
54
92
</Text>
55
93
</div>
56
94
</div>
57
95
</form>
58
96
</Card>
59
97
);
60
-
}
98
+
}
+47
-4
frontend/src/features/waitlist/templates/fragments/WaitlistSuccess.tsx
+47
-4
frontend/src/features/waitlist/templates/fragments/WaitlistSuccess.tsx
···
2
2
import { Card } from "../../../../shared/fragments/Card.tsx";
3
3
import { Text } from "../../../../shared/fragments/Text.tsx";
4
4
import { Link } from "../../../../shared/fragments/Link.tsx";
5
+
import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx";
5
6
import { Check } from "lucide-preact";
7
+
import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts";
6
8
7
9
interface WaitlistSuccessProps {
8
10
handle?: string;
11
+
recentRequests?: NetworkSlicesWaitlistDefsRequestView[];
9
12
}
10
13
11
-
export function WaitlistSuccess({ handle }: WaitlistSuccessProps) {
14
+
export function WaitlistSuccess({
15
+
handle,
16
+
recentRequests,
17
+
}: WaitlistSuccessProps) {
12
18
return (
13
-
<Card className="text-center">
19
+
<Card padding="md" className="text-center">
14
20
<div className="flex justify-center mb-6">
15
21
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
16
22
<Check size={32} className="text-green-600 dark:text-green-400" />
···
22
28
</Text>
23
29
24
30
<Text as="p" variant="secondary" className="mb-6">
25
-
Thanks for joining the waitlist{handle ? <>, <Text as="span" className="font-bold">{handle}</Text></> : ""}! We'll notify you as soon as Slices is ready for you.
31
+
Thanks for joining the waitlist
32
+
{handle ? (
33
+
<>
34
+
,{" "}
35
+
<Text as="span" className="font-bold">
36
+
{handle}
37
+
</Text>
38
+
</>
39
+
) : (
40
+
""
41
+
)}
42
+
! We'll notify you as soon as Slices is ready for you.
26
43
</Text>
27
44
45
+
{recentRequests && recentRequests.length > 0 && (
46
+
<div className="mb-6">
47
+
<Text as="p" size="sm" variant="muted" className="mb-3">
48
+
You've joined {recentRequests.length} others
49
+
</Text>
50
+
<div className="flex flex-wrap justify-center gap-1">
51
+
{recentRequests.slice(0, 30).map((request, index) => (
52
+
<div key={index} className="relative">
53
+
<ActorAvatar
54
+
profile={request.profile || { handle: "user" }}
55
+
size={32}
56
+
className="border border-white dark:border-zinc-800"
57
+
/>
58
+
</div>
59
+
))}
60
+
{recentRequests.length > 30 && (
61
+
<div className="w-8 h-8 bg-zinc-100 dark:bg-zinc-800 border border-white dark:border-zinc-800 rounded-full flex items-center justify-center">
62
+
<Text size="xs" variant="muted">
63
+
+{recentRequests.length - 30}
64
+
</Text>
65
+
</div>
66
+
)}
67
+
</div>
68
+
</div>
69
+
)}
70
+
28
71
<div className="space-y-4">
29
72
<Button href="/" variant="primary" className="w-full justify-center">
30
73
Back to Home
···
44
87
</div>
45
88
</Card>
46
89
);
47
-
}
90
+
}
+9
-3
frontend/src/lib/api.ts
+9
-3
frontend/src/lib/api.ts
···
176
176
if (creator) {
177
177
const sparklineData = sparklinesMap[sliceRecord.uri];
178
178
const statsData = statsMap[sliceRecord.uri];
179
-
sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData));
179
+
sliceViews.push(
180
+
sliceToView(sliceRecord, creator, sparklineData, statsData)
181
+
);
180
182
}
181
183
}
182
184
···
215
217
if (creator) {
216
218
const sparklineData = sparklinesMap[sliceRecord.uri];
217
219
const statsData = statsMap[sliceRecord.uri];
218
-
sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData));
220
+
sliceViews.push(
221
+
sliceToView(sliceRecord, creator, sparklineData, statsData)
222
+
);
219
223
}
220
224
}
221
225
···
251
255
if (creator) {
252
256
const sparklineData = sparklinesMap[sliceRecord.uri];
253
257
const statsData = statsMap[sliceRecord.uri];
254
-
sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData));
258
+
sliceViews.push(
259
+
sliceToView(sliceRecord, creator, sparklineData, statsData)
260
+
);
255
261
}
256
262
}
257
263
+5
-3
frontend/src/routes/mod.ts
+5
-3
frontend/src/routes/mod.ts
···
1
1
import type { Route } from "@std/http/unstable-route";
2
2
import { landingRoutes } from "../features/landing/handlers.tsx";
3
3
import { authRoutes } from "../features/auth/handlers.tsx";
4
-
import { waitlistRoutes } from "../features/waitlist/handlers.tsx";
5
4
import { dashboardRoutes } from "../features/dashboard/handlers.tsx";
5
+
import { waitlistRoutes as globalWaitlistRoutes } from "../features/waitlist/handlers.tsx";
6
6
import {
7
7
apiDocsRoutes,
8
8
codegenRoutes,
···
14
14
settingsRoutes as sliceSettingsRoutes,
15
15
syncLogsRoutes,
16
16
syncRoutes,
17
+
waitlistRoutes as sliceWaitlistRoutes,
17
18
} from "../features/slices/mod.ts";
18
19
import { settingsRoutes } from "../features/settings/handlers.tsx";
19
20
import { docsRoutes } from "../features/docs/handlers.tsx";
···
25
26
// Auth routes (login, oauth, logout)
26
27
...authRoutes,
27
28
28
-
// Waitlist page
29
-
...waitlistRoutes,
29
+
// Global waitlist page
30
+
...globalWaitlistRoutes,
30
31
31
32
// Documentation routes
32
33
...docsRoutes,
···
45
46
...syncRoutes,
46
47
...syncLogsRoutes,
47
48
...jetstreamRoutes,
49
+
...sliceWaitlistRoutes,
48
50
49
51
// Dashboard routes (home page, create slice)
50
52
...dashboardRoutes,
+25
frontend/src/utils/preact.ts
+25
frontend/src/utils/preact.ts
···
1
+
import type { ComponentChildren } from "preact";
2
+
import { cloneElement } from "preact";
3
+
4
+
/**
5
+
* Passes props down to all children that accept them
6
+
*/
7
+
export function passPropsToChildren(
8
+
children: ComponentChildren,
9
+
props: Record<string, unknown>
10
+
): ComponentChildren {
11
+
if (Array.isArray(children)) {
12
+
return children.map(child => {
13
+
if (child && typeof child === 'object' && 'type' in child) {
14
+
return cloneElement(child as any, props);
15
+
}
16
+
return child;
17
+
});
18
+
}
19
+
20
+
if (children && typeof children === 'object' && 'type' in children) {
21
+
return cloneElement(children as any, props);
22
+
}
23
+
24
+
return children;
25
+
}
+695
lexicons/app/bsky/actor/defs.json
+695
lexicons/app/bsky/actor/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.actor.defs",
4
+
"defs": {
5
+
"nux": {
6
+
"type": "object",
7
+
"required": [
8
+
"id",
9
+
"completed"
10
+
],
11
+
"properties": {
12
+
"id": {
13
+
"type": "string",
14
+
"maxLength": 100
15
+
},
16
+
"data": {
17
+
"type": "string",
18
+
"maxLength": 3000,
19
+
"description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.",
20
+
"maxGraphemes": 300
21
+
},
22
+
"completed": {
23
+
"type": "boolean",
24
+
"default": false
25
+
},
26
+
"expiresAt": {
27
+
"type": "string",
28
+
"format": "datetime",
29
+
"description": "The date and time at which the NUX will expire and should be considered completed."
30
+
}
31
+
},
32
+
"description": "A new user experiences (NUX) storage object"
33
+
},
34
+
"mutedWord": {
35
+
"type": "object",
36
+
"required": [
37
+
"value",
38
+
"targets"
39
+
],
40
+
"properties": {
41
+
"id": {
42
+
"type": "string"
43
+
},
44
+
"value": {
45
+
"type": "string",
46
+
"maxLength": 10000,
47
+
"description": "The muted word itself.",
48
+
"maxGraphemes": 1000
49
+
},
50
+
"targets": {
51
+
"type": "array",
52
+
"items": {
53
+
"ref": "app.bsky.actor.defs#mutedWordTarget",
54
+
"type": "ref"
55
+
},
56
+
"description": "The intended targets of the muted word."
57
+
},
58
+
"expiresAt": {
59
+
"type": "string",
60
+
"format": "datetime",
61
+
"description": "The date and time at which the muted word will expire and no longer be applied."
62
+
},
63
+
"actorTarget": {
64
+
"type": "string",
65
+
"default": "all",
66
+
"description": "Groups of users to apply the muted word to. If undefined, applies to all users.",
67
+
"knownValues": [
68
+
"all",
69
+
"exclude-following"
70
+
]
71
+
}
72
+
},
73
+
"description": "A word that the account owner has muted."
74
+
},
75
+
"savedFeed": {
76
+
"type": "object",
77
+
"required": [
78
+
"id",
79
+
"type",
80
+
"value",
81
+
"pinned"
82
+
],
83
+
"properties": {
84
+
"id": {
85
+
"type": "string"
86
+
},
87
+
"type": {
88
+
"type": "string",
89
+
"knownValues": [
90
+
"feed",
91
+
"list",
92
+
"timeline"
93
+
]
94
+
},
95
+
"value": {
96
+
"type": "string"
97
+
},
98
+
"pinned": {
99
+
"type": "boolean"
100
+
}
101
+
}
102
+
},
103
+
"preferences": {
104
+
"type": "array",
105
+
"items": {
106
+
"refs": [
107
+
"#adultContentPref",
108
+
"#contentLabelPref",
109
+
"#savedFeedsPref",
110
+
"#savedFeedsPrefV2",
111
+
"#personalDetailsPref",
112
+
"#feedViewPref",
113
+
"#threadViewPref",
114
+
"#interestsPref",
115
+
"#mutedWordsPref",
116
+
"#hiddenPostsPref",
117
+
"#bskyAppStatePref",
118
+
"#labelersPref",
119
+
"#postInteractionSettingsPref"
120
+
],
121
+
"type": "union"
122
+
}
123
+
},
124
+
"profileView": {
125
+
"type": "object",
126
+
"required": [
127
+
"did",
128
+
"handle"
129
+
],
130
+
"properties": {
131
+
"did": {
132
+
"type": "string",
133
+
"format": "did"
134
+
},
135
+
"avatar": {
136
+
"type": "string",
137
+
"format": "uri"
138
+
},
139
+
"handle": {
140
+
"type": "string",
141
+
"format": "handle"
142
+
},
143
+
"labels": {
144
+
"type": "array",
145
+
"items": {
146
+
"ref": "com.atproto.label.defs#label",
147
+
"type": "ref"
148
+
}
149
+
},
150
+
"viewer": {
151
+
"ref": "#viewerState",
152
+
"type": "ref"
153
+
},
154
+
"createdAt": {
155
+
"type": "string",
156
+
"format": "datetime"
157
+
},
158
+
"indexedAt": {
159
+
"type": "string",
160
+
"format": "datetime"
161
+
},
162
+
"associated": {
163
+
"ref": "#profileAssociated",
164
+
"type": "ref"
165
+
},
166
+
"description": {
167
+
"type": "string",
168
+
"maxLength": 2560,
169
+
"maxGraphemes": 256
170
+
},
171
+
"displayName": {
172
+
"type": "string",
173
+
"maxLength": 640,
174
+
"maxGraphemes": 64
175
+
}
176
+
}
177
+
},
178
+
"viewerState": {
179
+
"type": "object",
180
+
"properties": {
181
+
"muted": {
182
+
"type": "boolean"
183
+
},
184
+
"blocking": {
185
+
"type": "string",
186
+
"format": "at-uri"
187
+
},
188
+
"blockedBy": {
189
+
"type": "boolean"
190
+
},
191
+
"following": {
192
+
"type": "string",
193
+
"format": "at-uri"
194
+
},
195
+
"followedBy": {
196
+
"type": "string",
197
+
"format": "at-uri"
198
+
},
199
+
"mutedByList": {
200
+
"ref": "app.bsky.graph.defs#listViewBasic",
201
+
"type": "ref"
202
+
},
203
+
"blockingByList": {
204
+
"ref": "app.bsky.graph.defs#listViewBasic",
205
+
"type": "ref"
206
+
},
207
+
"knownFollowers": {
208
+
"ref": "#knownFollowers",
209
+
"type": "ref"
210
+
}
211
+
},
212
+
"description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests."
213
+
},
214
+
"feedViewPref": {
215
+
"type": "object",
216
+
"required": [
217
+
"feed"
218
+
],
219
+
"properties": {
220
+
"feed": {
221
+
"type": "string",
222
+
"description": "The URI of the feed, or an identifier which describes the feed."
223
+
},
224
+
"hideReplies": {
225
+
"type": "boolean",
226
+
"description": "Hide replies in the feed."
227
+
},
228
+
"hideReposts": {
229
+
"type": "boolean",
230
+
"description": "Hide reposts in the feed."
231
+
},
232
+
"hideQuotePosts": {
233
+
"type": "boolean",
234
+
"description": "Hide quote posts in the feed."
235
+
},
236
+
"hideRepliesByLikeCount": {
237
+
"type": "integer",
238
+
"description": "Hide replies in the feed if they do not have this number of likes."
239
+
},
240
+
"hideRepliesByUnfollowed": {
241
+
"type": "boolean",
242
+
"default": true,
243
+
"description": "Hide replies in the feed if they are not by followed users."
244
+
}
245
+
}
246
+
},
247
+
"labelersPref": {
248
+
"type": "object",
249
+
"required": [
250
+
"labelers"
251
+
],
252
+
"properties": {
253
+
"labelers": {
254
+
"type": "array",
255
+
"items": {
256
+
"ref": "#labelerPrefItem",
257
+
"type": "ref"
258
+
}
259
+
}
260
+
}
261
+
},
262
+
"interestsPref": {
263
+
"type": "object",
264
+
"required": [
265
+
"tags"
266
+
],
267
+
"properties": {
268
+
"tags": {
269
+
"type": "array",
270
+
"items": {
271
+
"type": "string",
272
+
"maxLength": 640,
273
+
"maxGraphemes": 64
274
+
},
275
+
"maxLength": 100,
276
+
"description": "A list of tags which describe the account owner's interests gathered during onboarding."
277
+
}
278
+
}
279
+
},
280
+
"knownFollowers": {
281
+
"type": "object",
282
+
"required": [
283
+
"count",
284
+
"followers"
285
+
],
286
+
"properties": {
287
+
"count": {
288
+
"type": "integer"
289
+
},
290
+
"followers": {
291
+
"type": "array",
292
+
"items": {
293
+
"ref": "#profileViewBasic",
294
+
"type": "ref"
295
+
},
296
+
"maxLength": 5,
297
+
"minLength": 0
298
+
}
299
+
},
300
+
"description": "The subject's followers whom you also follow"
301
+
},
302
+
"mutedWordsPref": {
303
+
"type": "object",
304
+
"required": [
305
+
"items"
306
+
],
307
+
"properties": {
308
+
"items": {
309
+
"type": "array",
310
+
"items": {
311
+
"ref": "app.bsky.actor.defs#mutedWord",
312
+
"type": "ref"
313
+
},
314
+
"description": "A list of words the account owner has muted."
315
+
}
316
+
}
317
+
},
318
+
"savedFeedsPref": {
319
+
"type": "object",
320
+
"required": [
321
+
"pinned",
322
+
"saved"
323
+
],
324
+
"properties": {
325
+
"saved": {
326
+
"type": "array",
327
+
"items": {
328
+
"type": "string",
329
+
"format": "at-uri"
330
+
}
331
+
},
332
+
"pinned": {
333
+
"type": "array",
334
+
"items": {
335
+
"type": "string",
336
+
"format": "at-uri"
337
+
}
338
+
},
339
+
"timelineIndex": {
340
+
"type": "integer"
341
+
}
342
+
}
343
+
},
344
+
"threadViewPref": {
345
+
"type": "object",
346
+
"properties": {
347
+
"sort": {
348
+
"type": "string",
349
+
"description": "Sorting mode for threads.",
350
+
"knownValues": [
351
+
"oldest",
352
+
"newest",
353
+
"most-likes",
354
+
"random",
355
+
"hotness"
356
+
]
357
+
},
358
+
"prioritizeFollowedUsers": {
359
+
"type": "boolean",
360
+
"description": "Show followed users at the top of all replies."
361
+
}
362
+
}
363
+
},
364
+
"hiddenPostsPref": {
365
+
"type": "object",
366
+
"required": [
367
+
"items"
368
+
],
369
+
"properties": {
370
+
"items": {
371
+
"type": "array",
372
+
"items": {
373
+
"type": "string",
374
+
"format": "at-uri"
375
+
},
376
+
"description": "A list of URIs of posts the account owner has hidden."
377
+
}
378
+
}
379
+
},
380
+
"labelerPrefItem": {
381
+
"type": "object",
382
+
"required": [
383
+
"did"
384
+
],
385
+
"properties": {
386
+
"did": {
387
+
"type": "string",
388
+
"format": "did"
389
+
}
390
+
}
391
+
},
392
+
"mutedWordTarget": {
393
+
"type": "string",
394
+
"maxLength": 640,
395
+
"knownValues": [
396
+
"content",
397
+
"tag"
398
+
],
399
+
"maxGraphemes": 64
400
+
},
401
+
"adultContentPref": {
402
+
"type": "object",
403
+
"required": [
404
+
"enabled"
405
+
],
406
+
"properties": {
407
+
"enabled": {
408
+
"type": "boolean",
409
+
"default": false
410
+
}
411
+
}
412
+
},
413
+
"bskyAppStatePref": {
414
+
"type": "object",
415
+
"properties": {
416
+
"nuxs": {
417
+
"type": "array",
418
+
"items": {
419
+
"ref": "app.bsky.actor.defs#nux",
420
+
"type": "ref"
421
+
},
422
+
"maxLength": 100,
423
+
"description": "Storage for NUXs the user has encountered."
424
+
},
425
+
"queuedNudges": {
426
+
"type": "array",
427
+
"items": {
428
+
"type": "string",
429
+
"maxLength": 100
430
+
},
431
+
"maxLength": 1000,
432
+
"description": "An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user."
433
+
},
434
+
"activeProgressGuide": {
435
+
"ref": "#bskyAppProgressGuide",
436
+
"type": "ref"
437
+
}
438
+
},
439
+
"description": "A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this."
440
+
},
441
+
"contentLabelPref": {
442
+
"type": "object",
443
+
"required": [
444
+
"label",
445
+
"visibility"
446
+
],
447
+
"properties": {
448
+
"label": {
449
+
"type": "string"
450
+
},
451
+
"labelerDid": {
452
+
"type": "string",
453
+
"format": "did",
454
+
"description": "Which labeler does this preference apply to? If undefined, applies globally."
455
+
},
456
+
"visibility": {
457
+
"type": "string",
458
+
"knownValues": [
459
+
"ignore",
460
+
"show",
461
+
"warn",
462
+
"hide"
463
+
]
464
+
}
465
+
}
466
+
},
467
+
"profileViewBasic": {
468
+
"type": "object",
469
+
"required": [
470
+
"did",
471
+
"handle"
472
+
],
473
+
"properties": {
474
+
"did": {
475
+
"type": "string",
476
+
"format": "did"
477
+
},
478
+
"avatar": {
479
+
"type": "string",
480
+
"format": "uri"
481
+
},
482
+
"handle": {
483
+
"type": "string",
484
+
"format": "handle"
485
+
},
486
+
"labels": {
487
+
"type": "array",
488
+
"items": {
489
+
"ref": "com.atproto.label.defs#label",
490
+
"type": "ref"
491
+
}
492
+
},
493
+
"viewer": {
494
+
"ref": "#viewerState",
495
+
"type": "ref"
496
+
},
497
+
"createdAt": {
498
+
"type": "string",
499
+
"format": "datetime"
500
+
},
501
+
"associated": {
502
+
"ref": "#profileAssociated",
503
+
"type": "ref"
504
+
},
505
+
"displayName": {
506
+
"type": "string",
507
+
"maxLength": 640,
508
+
"maxGraphemes": 64
509
+
}
510
+
}
511
+
},
512
+
"savedFeedsPrefV2": {
513
+
"type": "object",
514
+
"required": [
515
+
"items"
516
+
],
517
+
"properties": {
518
+
"items": {
519
+
"type": "array",
520
+
"items": {
521
+
"ref": "app.bsky.actor.defs#savedFeed",
522
+
"type": "ref"
523
+
}
524
+
}
525
+
}
526
+
},
527
+
"profileAssociated": {
528
+
"type": "object",
529
+
"properties": {
530
+
"chat": {
531
+
"ref": "#profileAssociatedChat",
532
+
"type": "ref"
533
+
},
534
+
"lists": {
535
+
"type": "integer"
536
+
},
537
+
"labeler": {
538
+
"type": "boolean"
539
+
},
540
+
"feedgens": {
541
+
"type": "integer"
542
+
},
543
+
"starterPacks": {
544
+
"type": "integer"
545
+
}
546
+
}
547
+
},
548
+
"personalDetailsPref": {
549
+
"type": "object",
550
+
"properties": {
551
+
"birthDate": {
552
+
"type": "string",
553
+
"format": "datetime",
554
+
"description": "The birth date of account owner."
555
+
}
556
+
}
557
+
},
558
+
"profileViewDetailed": {
559
+
"type": "object",
560
+
"required": [
561
+
"did",
562
+
"handle"
563
+
],
564
+
"properties": {
565
+
"did": {
566
+
"type": "string",
567
+
"format": "did"
568
+
},
569
+
"avatar": {
570
+
"type": "string",
571
+
"format": "uri"
572
+
},
573
+
"banner": {
574
+
"type": "string",
575
+
"format": "uri"
576
+
},
577
+
"handle": {
578
+
"type": "string",
579
+
"format": "handle"
580
+
},
581
+
"labels": {
582
+
"type": "array",
583
+
"items": {
584
+
"ref": "com.atproto.label.defs#label",
585
+
"type": "ref"
586
+
}
587
+
},
588
+
"viewer": {
589
+
"ref": "#viewerState",
590
+
"type": "ref"
591
+
},
592
+
"createdAt": {
593
+
"type": "string",
594
+
"format": "datetime"
595
+
},
596
+
"indexedAt": {
597
+
"type": "string",
598
+
"format": "datetime"
599
+
},
600
+
"associated": {
601
+
"ref": "#profileAssociated",
602
+
"type": "ref"
603
+
},
604
+
"pinnedPost": {
605
+
"ref": "com.atproto.repo.strongRef",
606
+
"type": "ref"
607
+
},
608
+
"postsCount": {
609
+
"type": "integer"
610
+
},
611
+
"description": {
612
+
"type": "string",
613
+
"maxLength": 2560,
614
+
"maxGraphemes": 256
615
+
},
616
+
"displayName": {
617
+
"type": "string",
618
+
"maxLength": 640,
619
+
"maxGraphemes": 64
620
+
},
621
+
"followsCount": {
622
+
"type": "integer"
623
+
},
624
+
"followersCount": {
625
+
"type": "integer"
626
+
},
627
+
"joinedViaStarterPack": {
628
+
"ref": "app.bsky.graph.defs#starterPackViewBasic",
629
+
"type": "ref"
630
+
}
631
+
}
632
+
},
633
+
"bskyAppProgressGuide": {
634
+
"type": "object",
635
+
"required": [
636
+
"guide"
637
+
],
638
+
"properties": {
639
+
"guide": {
640
+
"type": "string",
641
+
"maxLength": 100
642
+
}
643
+
},
644
+
"description": "If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress."
645
+
},
646
+
"profileAssociatedChat": {
647
+
"type": "object",
648
+
"required": [
649
+
"allowIncoming"
650
+
],
651
+
"properties": {
652
+
"allowIncoming": {
653
+
"type": "string",
654
+
"knownValues": [
655
+
"all",
656
+
"none",
657
+
"following"
658
+
]
659
+
}
660
+
}
661
+
},
662
+
"postInteractionSettingsPref": {
663
+
"type": "object",
664
+
"required": [],
665
+
"properties": {
666
+
"threadgateAllowRules": {
667
+
"type": "array",
668
+
"items": {
669
+
"refs": [
670
+
"app.bsky.feed.threadgate#mentionRule",
671
+
"app.bsky.feed.threadgate#followerRule",
672
+
"app.bsky.feed.threadgate#followingRule",
673
+
"app.bsky.feed.threadgate#listRule"
674
+
],
675
+
"type": "union"
676
+
},
677
+
"maxLength": 5,
678
+
"description": "Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply."
679
+
},
680
+
"postgateEmbeddingRules": {
681
+
"type": "array",
682
+
"items": {
683
+
"refs": [
684
+
"app.bsky.feed.postgate#disableRule"
685
+
],
686
+
"type": "union"
687
+
},
688
+
"maxLength": 5,
689
+
"description": "Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed."
690
+
}
691
+
},
692
+
"description": "Default post interaction settings for the account. These values should be applied as default values when creating new posts. These refs should mirror the threadgate and postgate records exactly."
693
+
}
694
+
}
695
+
}
+12
-4
lexicons/app/bsky/actor/profile.json
+12
-4
lexicons/app/bsky/actor/profile.json
···
10
10
"properties": {
11
11
"avatar": {
12
12
"type": "blob",
13
-
"accept": ["image/png", "image/jpeg"],
13
+
"accept": [
14
+
"image/png",
15
+
"image/jpeg"
16
+
],
14
17
"maxSize": 1000000,
15
18
"description": "Small image to be displayed next to posts from account. AKA, 'profile picture'"
16
19
},
17
20
"banner": {
18
21
"type": "blob",
19
-
"accept": ["image/png", "image/jpeg"],
22
+
"accept": [
23
+
"image/png",
24
+
"image/jpeg"
25
+
],
20
26
"maxSize": 1000000,
21
27
"description": "Larger horizontal image to display behind profile view."
22
28
},
23
29
"labels": {
24
-
"refs": ["com.atproto.label.defs#selfLabels"],
30
+
"refs": [
31
+
"com.atproto.label.defs#selfLabels"
32
+
],
25
33
"type": "union",
26
34
"description": "Self-label values, specific to the Bluesky application, on the overall account."
27
35
},
···
53
61
"description": "A declaration of a Bluesky account profile."
54
62
}
55
63
}
56
-
}
64
+
}
+24
lexicons/app/bsky/embed/defs.json
+24
lexicons/app/bsky/embed/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.defs",
4
+
"defs": {
5
+
"aspectRatio": {
6
+
"type": "object",
7
+
"required": [
8
+
"width",
9
+
"height"
10
+
],
11
+
"properties": {
12
+
"width": {
13
+
"type": "integer",
14
+
"minimum": 1
15
+
},
16
+
"height": {
17
+
"type": "integer",
18
+
"minimum": 1
19
+
}
20
+
},
21
+
"description": "width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit."
22
+
}
23
+
}
24
+
}
+82
lexicons/app/bsky/embed/external.json
+82
lexicons/app/bsky/embed/external.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.external",
4
+
"defs": {
5
+
"main": {
6
+
"type": "object",
7
+
"required": [
8
+
"external"
9
+
],
10
+
"properties": {
11
+
"external": {
12
+
"ref": "#external",
13
+
"type": "ref"
14
+
}
15
+
},
16
+
"description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post)."
17
+
},
18
+
"view": {
19
+
"type": "object",
20
+
"required": [
21
+
"external"
22
+
],
23
+
"properties": {
24
+
"external": {
25
+
"ref": "#viewExternal",
26
+
"type": "ref"
27
+
}
28
+
}
29
+
},
30
+
"external": {
31
+
"type": "object",
32
+
"required": [
33
+
"uri",
34
+
"title",
35
+
"description"
36
+
],
37
+
"properties": {
38
+
"uri": {
39
+
"type": "string",
40
+
"format": "uri"
41
+
},
42
+
"thumb": {
43
+
"type": "blob",
44
+
"accept": [
45
+
"image/*"
46
+
],
47
+
"maxSize": 1000000
48
+
},
49
+
"title": {
50
+
"type": "string"
51
+
},
52
+
"description": {
53
+
"type": "string"
54
+
}
55
+
}
56
+
},
57
+
"viewExternal": {
58
+
"type": "object",
59
+
"required": [
60
+
"uri",
61
+
"title",
62
+
"description"
63
+
],
64
+
"properties": {
65
+
"uri": {
66
+
"type": "string",
67
+
"format": "uri"
68
+
},
69
+
"thumb": {
70
+
"type": "string",
71
+
"format": "uri"
72
+
},
73
+
"title": {
74
+
"type": "string"
75
+
},
76
+
"description": {
77
+
"type": "string"
78
+
}
79
+
}
80
+
}
81
+
}
82
+
}
+91
lexicons/app/bsky/embed/images.json
+91
lexicons/app/bsky/embed/images.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.images",
4
+
"description": "A set of images embedded in a Bluesky record (eg, a post).",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": [
9
+
"images"
10
+
],
11
+
"properties": {
12
+
"images": {
13
+
"type": "array",
14
+
"items": {
15
+
"ref": "#image",
16
+
"type": "ref"
17
+
},
18
+
"maxLength": 4
19
+
}
20
+
}
21
+
},
22
+
"view": {
23
+
"type": "object",
24
+
"required": [
25
+
"images"
26
+
],
27
+
"properties": {
28
+
"images": {
29
+
"type": "array",
30
+
"items": {
31
+
"ref": "#viewImage",
32
+
"type": "ref"
33
+
},
34
+
"maxLength": 4
35
+
}
36
+
}
37
+
},
38
+
"image": {
39
+
"type": "object",
40
+
"required": [
41
+
"image",
42
+
"alt"
43
+
],
44
+
"properties": {
45
+
"alt": {
46
+
"type": "string",
47
+
"description": "Alt text description of the image, for accessibility."
48
+
},
49
+
"image": {
50
+
"type": "blob",
51
+
"accept": [
52
+
"image/*"
53
+
],
54
+
"maxSize": 1000000
55
+
},
56
+
"aspectRatio": {
57
+
"ref": "app.bsky.embed.defs#aspectRatio",
58
+
"type": "ref"
59
+
}
60
+
}
61
+
},
62
+
"viewImage": {
63
+
"type": "object",
64
+
"required": [
65
+
"thumb",
66
+
"fullsize",
67
+
"alt"
68
+
],
69
+
"properties": {
70
+
"alt": {
71
+
"type": "string",
72
+
"description": "Alt text description of the image, for accessibility."
73
+
},
74
+
"thumb": {
75
+
"type": "string",
76
+
"format": "uri",
77
+
"description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View."
78
+
},
79
+
"fullsize": {
80
+
"type": "string",
81
+
"format": "uri",
82
+
"description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View."
83
+
},
84
+
"aspectRatio": {
85
+
"ref": "app.bsky.embed.defs#aspectRatio",
86
+
"type": "ref"
87
+
}
88
+
}
89
+
}
90
+
}
91
+
}
+160
lexicons/app/bsky/embed/record.json
+160
lexicons/app/bsky/embed/record.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.record",
4
+
"description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": [
9
+
"record"
10
+
],
11
+
"properties": {
12
+
"record": {
13
+
"ref": "com.atproto.repo.strongRef",
14
+
"type": "ref"
15
+
}
16
+
}
17
+
},
18
+
"view": {
19
+
"type": "object",
20
+
"required": [
21
+
"record"
22
+
],
23
+
"properties": {
24
+
"record": {
25
+
"refs": [
26
+
"#viewRecord",
27
+
"#viewNotFound",
28
+
"#viewBlocked",
29
+
"#viewDetached",
30
+
"app.bsky.feed.defs#generatorView",
31
+
"app.bsky.graph.defs#listView",
32
+
"app.bsky.labeler.defs#labelerView",
33
+
"app.bsky.graph.defs#starterPackViewBasic"
34
+
],
35
+
"type": "union"
36
+
}
37
+
}
38
+
},
39
+
"viewRecord": {
40
+
"type": "object",
41
+
"required": [
42
+
"uri",
43
+
"cid",
44
+
"author",
45
+
"value",
46
+
"indexedAt"
47
+
],
48
+
"properties": {
49
+
"cid": {
50
+
"type": "string",
51
+
"format": "cid"
52
+
},
53
+
"uri": {
54
+
"type": "string",
55
+
"format": "at-uri"
56
+
},
57
+
"value": {
58
+
"type": "unknown",
59
+
"description": "The record data itself."
60
+
},
61
+
"author": {
62
+
"ref": "app.bsky.actor.defs#profileViewBasic",
63
+
"type": "ref"
64
+
},
65
+
"embeds": {
66
+
"type": "array",
67
+
"items": {
68
+
"refs": [
69
+
"app.bsky.embed.images#view",
70
+
"app.bsky.embed.video#view",
71
+
"app.bsky.embed.external#view",
72
+
"app.bsky.embed.record#view",
73
+
"app.bsky.embed.recordWithMedia#view"
74
+
],
75
+
"type": "union"
76
+
}
77
+
},
78
+
"labels": {
79
+
"type": "array",
80
+
"items": {
81
+
"ref": "com.atproto.label.defs#label",
82
+
"type": "ref"
83
+
}
84
+
},
85
+
"indexedAt": {
86
+
"type": "string",
87
+
"format": "datetime"
88
+
},
89
+
"likeCount": {
90
+
"type": "integer"
91
+
},
92
+
"quoteCount": {
93
+
"type": "integer"
94
+
},
95
+
"replyCount": {
96
+
"type": "integer"
97
+
},
98
+
"repostCount": {
99
+
"type": "integer"
100
+
}
101
+
}
102
+
},
103
+
"viewBlocked": {
104
+
"type": "object",
105
+
"required": [
106
+
"uri",
107
+
"blocked",
108
+
"author"
109
+
],
110
+
"properties": {
111
+
"uri": {
112
+
"type": "string",
113
+
"format": "at-uri"
114
+
},
115
+
"author": {
116
+
"ref": "app.bsky.feed.defs#blockedAuthor",
117
+
"type": "ref"
118
+
},
119
+
"blocked": {
120
+
"type": "boolean",
121
+
"const": true
122
+
}
123
+
}
124
+
},
125
+
"viewDetached": {
126
+
"type": "object",
127
+
"required": [
128
+
"uri",
129
+
"detached"
130
+
],
131
+
"properties": {
132
+
"uri": {
133
+
"type": "string",
134
+
"format": "at-uri"
135
+
},
136
+
"detached": {
137
+
"type": "boolean",
138
+
"const": true
139
+
}
140
+
}
141
+
},
142
+
"viewNotFound": {
143
+
"type": "object",
144
+
"required": [
145
+
"uri",
146
+
"notFound"
147
+
],
148
+
"properties": {
149
+
"uri": {
150
+
"type": "string",
151
+
"format": "at-uri"
152
+
},
153
+
"notFound": {
154
+
"type": "boolean",
155
+
"const": true
156
+
}
157
+
}
158
+
}
159
+
}
160
+
}
+49
lexicons/app/bsky/embed/recordWithMedia.json
+49
lexicons/app/bsky/embed/recordWithMedia.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.recordWithMedia",
4
+
"description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": [
9
+
"record",
10
+
"media"
11
+
],
12
+
"properties": {
13
+
"media": {
14
+
"refs": [
15
+
"app.bsky.embed.images",
16
+
"app.bsky.embed.video",
17
+
"app.bsky.embed.external"
18
+
],
19
+
"type": "union"
20
+
},
21
+
"record": {
22
+
"ref": "app.bsky.embed.record",
23
+
"type": "ref"
24
+
}
25
+
}
26
+
},
27
+
"view": {
28
+
"type": "object",
29
+
"required": [
30
+
"record",
31
+
"media"
32
+
],
33
+
"properties": {
34
+
"media": {
35
+
"refs": [
36
+
"app.bsky.embed.images#view",
37
+
"app.bsky.embed.video#view",
38
+
"app.bsky.embed.external#view"
39
+
],
40
+
"type": "union"
41
+
},
42
+
"record": {
43
+
"ref": "app.bsky.embed.record#view",
44
+
"type": "ref"
45
+
}
46
+
}
47
+
}
48
+
}
49
+
}
+90
lexicons/app/bsky/embed/video.json
+90
lexicons/app/bsky/embed/video.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.embed.video",
4
+
"description": "A video embedded in a Bluesky record (eg, a post).",
5
+
"defs": {
6
+
"main": {
7
+
"type": "object",
8
+
"required": [
9
+
"video"
10
+
],
11
+
"properties": {
12
+
"alt": {
13
+
"type": "string",
14
+
"maxLength": 10000,
15
+
"description": "Alt text description of the video, for accessibility.",
16
+
"maxGraphemes": 1000
17
+
},
18
+
"video": {
19
+
"type": "blob",
20
+
"accept": [
21
+
"video/mp4"
22
+
],
23
+
"maxSize": 50000000
24
+
},
25
+
"captions": {
26
+
"type": "array",
27
+
"items": {
28
+
"ref": "#caption",
29
+
"type": "ref"
30
+
},
31
+
"maxLength": 20
32
+
},
33
+
"aspectRatio": {
34
+
"ref": "app.bsky.embed.defs#aspectRatio",
35
+
"type": "ref"
36
+
}
37
+
}
38
+
},
39
+
"view": {
40
+
"type": "object",
41
+
"required": [
42
+
"cid",
43
+
"playlist"
44
+
],
45
+
"properties": {
46
+
"alt": {
47
+
"type": "string",
48
+
"maxLength": 10000,
49
+
"maxGraphemes": 1000
50
+
},
51
+
"cid": {
52
+
"type": "string",
53
+
"format": "cid"
54
+
},
55
+
"playlist": {
56
+
"type": "string",
57
+
"format": "uri"
58
+
},
59
+
"thumbnail": {
60
+
"type": "string",
61
+
"format": "uri"
62
+
},
63
+
"aspectRatio": {
64
+
"ref": "app.bsky.embed.defs#aspectRatio",
65
+
"type": "ref"
66
+
}
67
+
}
68
+
},
69
+
"caption": {
70
+
"type": "object",
71
+
"required": [
72
+
"lang",
73
+
"file"
74
+
],
75
+
"properties": {
76
+
"file": {
77
+
"type": "blob",
78
+
"accept": [
79
+
"text/vtt"
80
+
],
81
+
"maxSize": 20000
82
+
},
83
+
"lang": {
84
+
"type": "string",
85
+
"format": "language"
86
+
}
87
+
}
88
+
}
89
+
}
90
+
}
+515
lexicons/app/bsky/feed/defs.json
+515
lexicons/app/bsky/feed/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.defs",
4
+
"defs": {
5
+
"postView": {
6
+
"type": "object",
7
+
"required": [
8
+
"uri",
9
+
"cid",
10
+
"author",
11
+
"record",
12
+
"indexedAt"
13
+
],
14
+
"properties": {
15
+
"cid": {
16
+
"type": "string",
17
+
"format": "cid"
18
+
},
19
+
"uri": {
20
+
"type": "string",
21
+
"format": "at-uri"
22
+
},
23
+
"embed": {
24
+
"refs": [
25
+
"app.bsky.embed.images#view",
26
+
"app.bsky.embed.video#view",
27
+
"app.bsky.embed.external#view",
28
+
"app.bsky.embed.record#view",
29
+
"app.bsky.embed.recordWithMedia#view"
30
+
],
31
+
"type": "union"
32
+
},
33
+
"author": {
34
+
"ref": "app.bsky.actor.defs#profileViewBasic",
35
+
"type": "ref"
36
+
},
37
+
"labels": {
38
+
"type": "array",
39
+
"items": {
40
+
"ref": "com.atproto.label.defs#label",
41
+
"type": "ref"
42
+
}
43
+
},
44
+
"record": {
45
+
"type": "unknown"
46
+
},
47
+
"viewer": {
48
+
"ref": "#viewerState",
49
+
"type": "ref"
50
+
},
51
+
"indexedAt": {
52
+
"type": "string",
53
+
"format": "datetime"
54
+
},
55
+
"likeCount": {
56
+
"type": "integer"
57
+
},
58
+
"quoteCount": {
59
+
"type": "integer"
60
+
},
61
+
"replyCount": {
62
+
"type": "integer"
63
+
},
64
+
"threadgate": {
65
+
"ref": "#threadgateView",
66
+
"type": "ref"
67
+
},
68
+
"repostCount": {
69
+
"type": "integer"
70
+
}
71
+
}
72
+
},
73
+
"replyRef": {
74
+
"type": "object",
75
+
"required": [
76
+
"root",
77
+
"parent"
78
+
],
79
+
"properties": {
80
+
"root": {
81
+
"refs": [
82
+
"#postView",
83
+
"#notFoundPost",
84
+
"#blockedPost"
85
+
],
86
+
"type": "union"
87
+
},
88
+
"parent": {
89
+
"refs": [
90
+
"#postView",
91
+
"#notFoundPost",
92
+
"#blockedPost"
93
+
],
94
+
"type": "union"
95
+
},
96
+
"grandparentAuthor": {
97
+
"ref": "app.bsky.actor.defs#profileViewBasic",
98
+
"type": "ref",
99
+
"description": "When parent is a reply to another post, this is the author of that post."
100
+
}
101
+
}
102
+
},
103
+
"reasonPin": {
104
+
"type": "object",
105
+
"properties": {}
106
+
},
107
+
"blockedPost": {
108
+
"type": "object",
109
+
"required": [
110
+
"uri",
111
+
"blocked",
112
+
"author"
113
+
],
114
+
"properties": {
115
+
"uri": {
116
+
"type": "string",
117
+
"format": "at-uri"
118
+
},
119
+
"author": {
120
+
"ref": "#blockedAuthor",
121
+
"type": "ref"
122
+
},
123
+
"blocked": {
124
+
"type": "boolean",
125
+
"const": true
126
+
}
127
+
}
128
+
},
129
+
"interaction": {
130
+
"type": "object",
131
+
"properties": {
132
+
"item": {
133
+
"type": "string",
134
+
"format": "at-uri"
135
+
},
136
+
"event": {
137
+
"type": "string",
138
+
"knownValues": [
139
+
"app.bsky.feed.defs#requestLess",
140
+
"app.bsky.feed.defs#requestMore",
141
+
"app.bsky.feed.defs#clickthroughItem",
142
+
"app.bsky.feed.defs#clickthroughAuthor",
143
+
"app.bsky.feed.defs#clickthroughReposter",
144
+
"app.bsky.feed.defs#clickthroughEmbed",
145
+
"app.bsky.feed.defs#interactionSeen",
146
+
"app.bsky.feed.defs#interactionLike",
147
+
"app.bsky.feed.defs#interactionRepost",
148
+
"app.bsky.feed.defs#interactionReply",
149
+
"app.bsky.feed.defs#interactionQuote",
150
+
"app.bsky.feed.defs#interactionShare"
151
+
]
152
+
},
153
+
"feedContext": {
154
+
"type": "string",
155
+
"maxLength": 2000,
156
+
"description": "Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton."
157
+
}
158
+
}
159
+
},
160
+
"requestLess": {
161
+
"type": "token",
162
+
"description": "Request that less content like the given feed item be shown in the feed"
163
+
},
164
+
"requestMore": {
165
+
"type": "token",
166
+
"description": "Request that more content like the given feed item be shown in the feed"
167
+
},
168
+
"viewerState": {
169
+
"type": "object",
170
+
"properties": {
171
+
"like": {
172
+
"type": "string",
173
+
"format": "at-uri"
174
+
},
175
+
"pinned": {
176
+
"type": "boolean"
177
+
},
178
+
"repost": {
179
+
"type": "string",
180
+
"format": "at-uri"
181
+
},
182
+
"threadMuted": {
183
+
"type": "boolean"
184
+
},
185
+
"replyDisabled": {
186
+
"type": "boolean"
187
+
},
188
+
"embeddingDisabled": {
189
+
"type": "boolean"
190
+
}
191
+
},
192
+
"description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests."
193
+
},
194
+
"feedViewPost": {
195
+
"type": "object",
196
+
"required": [
197
+
"post"
198
+
],
199
+
"properties": {
200
+
"post": {
201
+
"ref": "#postView",
202
+
"type": "ref"
203
+
},
204
+
"reply": {
205
+
"ref": "#replyRef",
206
+
"type": "ref"
207
+
},
208
+
"reason": {
209
+
"refs": [
210
+
"#reasonRepost",
211
+
"#reasonPin"
212
+
],
213
+
"type": "union"
214
+
},
215
+
"feedContext": {
216
+
"type": "string",
217
+
"maxLength": 2000,
218
+
"description": "Context provided by feed generator that may be passed back alongside interactions."
219
+
}
220
+
}
221
+
},
222
+
"notFoundPost": {
223
+
"type": "object",
224
+
"required": [
225
+
"uri",
226
+
"notFound"
227
+
],
228
+
"properties": {
229
+
"uri": {
230
+
"type": "string",
231
+
"format": "at-uri"
232
+
},
233
+
"notFound": {
234
+
"type": "boolean",
235
+
"const": true
236
+
}
237
+
}
238
+
},
239
+
"reasonRepost": {
240
+
"type": "object",
241
+
"required": [
242
+
"by",
243
+
"indexedAt"
244
+
],
245
+
"properties": {
246
+
"by": {
247
+
"ref": "app.bsky.actor.defs#profileViewBasic",
248
+
"type": "ref"
249
+
},
250
+
"indexedAt": {
251
+
"type": "string",
252
+
"format": "datetime"
253
+
}
254
+
}
255
+
},
256
+
"blockedAuthor": {
257
+
"type": "object",
258
+
"required": [
259
+
"did"
260
+
],
261
+
"properties": {
262
+
"did": {
263
+
"type": "string",
264
+
"format": "did"
265
+
},
266
+
"viewer": {
267
+
"ref": "app.bsky.actor.defs#viewerState",
268
+
"type": "ref"
269
+
}
270
+
}
271
+
},
272
+
"generatorView": {
273
+
"type": "object",
274
+
"required": [
275
+
"uri",
276
+
"cid",
277
+
"did",
278
+
"creator",
279
+
"displayName",
280
+
"indexedAt"
281
+
],
282
+
"properties": {
283
+
"cid": {
284
+
"type": "string",
285
+
"format": "cid"
286
+
},
287
+
"did": {
288
+
"type": "string",
289
+
"format": "did"
290
+
},
291
+
"uri": {
292
+
"type": "string",
293
+
"format": "at-uri"
294
+
},
295
+
"avatar": {
296
+
"type": "string",
297
+
"format": "uri"
298
+
},
299
+
"labels": {
300
+
"type": "array",
301
+
"items": {
302
+
"ref": "com.atproto.label.defs#label",
303
+
"type": "ref"
304
+
}
305
+
},
306
+
"viewer": {
307
+
"ref": "#generatorViewerState",
308
+
"type": "ref"
309
+
},
310
+
"creator": {
311
+
"ref": "app.bsky.actor.defs#profileView",
312
+
"type": "ref"
313
+
},
314
+
"indexedAt": {
315
+
"type": "string",
316
+
"format": "datetime"
317
+
},
318
+
"likeCount": {
319
+
"type": "integer",
320
+
"minimum": 0
321
+
},
322
+
"contentMode": {
323
+
"type": "string",
324
+
"knownValues": [
325
+
"app.bsky.feed.defs#contentModeUnspecified",
326
+
"app.bsky.feed.defs#contentModeVideo"
327
+
]
328
+
},
329
+
"description": {
330
+
"type": "string",
331
+
"maxLength": 3000,
332
+
"maxGraphemes": 300
333
+
},
334
+
"displayName": {
335
+
"type": "string"
336
+
},
337
+
"descriptionFacets": {
338
+
"type": "array",
339
+
"items": {
340
+
"ref": "app.bsky.richtext.facet",
341
+
"type": "ref"
342
+
}
343
+
},
344
+
"acceptsInteractions": {
345
+
"type": "boolean"
346
+
}
347
+
}
348
+
},
349
+
"threadContext": {
350
+
"type": "object",
351
+
"properties": {
352
+
"rootAuthorLike": {
353
+
"type": "string",
354
+
"format": "at-uri"
355
+
}
356
+
},
357
+
"description": "Metadata about this post within the context of the thread it is in."
358
+
},
359
+
"threadViewPost": {
360
+
"type": "object",
361
+
"required": [
362
+
"post"
363
+
],
364
+
"properties": {
365
+
"post": {
366
+
"ref": "#postView",
367
+
"type": "ref"
368
+
},
369
+
"parent": {
370
+
"refs": [
371
+
"#threadViewPost",
372
+
"#notFoundPost",
373
+
"#blockedPost"
374
+
],
375
+
"type": "union"
376
+
},
377
+
"replies": {
378
+
"type": "array",
379
+
"items": {
380
+
"refs": [
381
+
"#threadViewPost",
382
+
"#notFoundPost",
383
+
"#blockedPost"
384
+
],
385
+
"type": "union"
386
+
}
387
+
},
388
+
"threadContext": {
389
+
"ref": "#threadContext",
390
+
"type": "ref"
391
+
}
392
+
}
393
+
},
394
+
"threadgateView": {
395
+
"type": "object",
396
+
"properties": {
397
+
"cid": {
398
+
"type": "string",
399
+
"format": "cid"
400
+
},
401
+
"uri": {
402
+
"type": "string",
403
+
"format": "at-uri"
404
+
},
405
+
"lists": {
406
+
"type": "array",
407
+
"items": {
408
+
"ref": "app.bsky.graph.defs#listViewBasic",
409
+
"type": "ref"
410
+
}
411
+
},
412
+
"record": {
413
+
"type": "unknown"
414
+
}
415
+
}
416
+
},
417
+
"interactionLike": {
418
+
"type": "token",
419
+
"description": "User liked the feed item"
420
+
},
421
+
"interactionSeen": {
422
+
"type": "token",
423
+
"description": "Feed item was seen by user"
424
+
},
425
+
"clickthroughItem": {
426
+
"type": "token",
427
+
"description": "User clicked through to the feed item"
428
+
},
429
+
"contentModeVideo": {
430
+
"type": "token",
431
+
"description": "Declares the feed generator returns posts containing app.bsky.embed.video embeds."
432
+
},
433
+
"interactionQuote": {
434
+
"type": "token",
435
+
"description": "User quoted the feed item"
436
+
},
437
+
"interactionReply": {
438
+
"type": "token",
439
+
"description": "User replied to the feed item"
440
+
},
441
+
"interactionShare": {
442
+
"type": "token",
443
+
"description": "User shared the feed item"
444
+
},
445
+
"skeletonFeedPost": {
446
+
"type": "object",
447
+
"required": [
448
+
"post"
449
+
],
450
+
"properties": {
451
+
"post": {
452
+
"type": "string",
453
+
"format": "at-uri"
454
+
},
455
+
"reason": {
456
+
"refs": [
457
+
"#skeletonReasonRepost",
458
+
"#skeletonReasonPin"
459
+
],
460
+
"type": "union"
461
+
},
462
+
"feedContext": {
463
+
"type": "string",
464
+
"maxLength": 2000,
465
+
"description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions."
466
+
}
467
+
}
468
+
},
469
+
"clickthroughEmbed": {
470
+
"type": "token",
471
+
"description": "User clicked through to the embedded content of the feed item"
472
+
},
473
+
"interactionRepost": {
474
+
"type": "token",
475
+
"description": "User reposted the feed item"
476
+
},
477
+
"skeletonReasonPin": {
478
+
"type": "object",
479
+
"properties": {}
480
+
},
481
+
"clickthroughAuthor": {
482
+
"type": "token",
483
+
"description": "User clicked through to the author of the feed item"
484
+
},
485
+
"clickthroughReposter": {
486
+
"type": "token",
487
+
"description": "User clicked through to the reposter of the feed item"
488
+
},
489
+
"generatorViewerState": {
490
+
"type": "object",
491
+
"properties": {
492
+
"like": {
493
+
"type": "string",
494
+
"format": "at-uri"
495
+
}
496
+
}
497
+
},
498
+
"skeletonReasonRepost": {
499
+
"type": "object",
500
+
"required": [
501
+
"repost"
502
+
],
503
+
"properties": {
504
+
"repost": {
505
+
"type": "string",
506
+
"format": "at-uri"
507
+
}
508
+
}
509
+
},
510
+
"contentModeUnspecified": {
511
+
"type": "token",
512
+
"description": "Declares the feed generator returns any types of posts."
513
+
}
514
+
}
515
+
}
+54
lexicons/app/bsky/feed/postgate.json
+54
lexicons/app/bsky/feed/postgate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.postgate",
4
+
"defs": {
5
+
"main": {
6
+
"key": "tid",
7
+
"type": "record",
8
+
"record": {
9
+
"type": "object",
10
+
"required": [
11
+
"post",
12
+
"createdAt"
13
+
],
14
+
"properties": {
15
+
"post": {
16
+
"type": "string",
17
+
"format": "at-uri",
18
+
"description": "Reference (AT-URI) to the post record."
19
+
},
20
+
"createdAt": {
21
+
"type": "string",
22
+
"format": "datetime"
23
+
},
24
+
"embeddingRules": {
25
+
"type": "array",
26
+
"items": {
27
+
"refs": [
28
+
"#disableRule"
29
+
],
30
+
"type": "union"
31
+
},
32
+
"maxLength": 5,
33
+
"description": "List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed."
34
+
},
35
+
"detachedEmbeddingUris": {
36
+
"type": "array",
37
+
"items": {
38
+
"type": "string",
39
+
"format": "at-uri"
40
+
},
41
+
"maxLength": 50,
42
+
"description": "List of AT-URIs embedding this post that the author has detached from."
43
+
}
44
+
}
45
+
},
46
+
"description": "Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository."
47
+
},
48
+
"disableRule": {
49
+
"type": "object",
50
+
"properties": {},
51
+
"description": "Disables embedding of this post."
52
+
}
53
+
}
54
+
}
+80
lexicons/app/bsky/feed/threadgate.json
+80
lexicons/app/bsky/feed/threadgate.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.feed.threadgate",
4
+
"defs": {
5
+
"main": {
6
+
"key": "tid",
7
+
"type": "record",
8
+
"record": {
9
+
"type": "object",
10
+
"required": [
11
+
"post",
12
+
"createdAt"
13
+
],
14
+
"properties": {
15
+
"post": {
16
+
"type": "string",
17
+
"format": "at-uri",
18
+
"description": "Reference (AT-URI) to the post record."
19
+
},
20
+
"allow": {
21
+
"type": "array",
22
+
"items": {
23
+
"refs": [
24
+
"#mentionRule",
25
+
"#followerRule",
26
+
"#followingRule",
27
+
"#listRule"
28
+
],
29
+
"type": "union"
30
+
},
31
+
"maxLength": 5,
32
+
"description": "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply."
33
+
},
34
+
"createdAt": {
35
+
"type": "string",
36
+
"format": "datetime"
37
+
},
38
+
"hiddenReplies": {
39
+
"type": "array",
40
+
"items": {
41
+
"type": "string",
42
+
"format": "at-uri"
43
+
},
44
+
"maxLength": 50,
45
+
"description": "List of hidden reply URIs."
46
+
}
47
+
}
48
+
},
49
+
"description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository."
50
+
},
51
+
"listRule": {
52
+
"type": "object",
53
+
"required": [
54
+
"list"
55
+
],
56
+
"properties": {
57
+
"list": {
58
+
"type": "string",
59
+
"format": "at-uri"
60
+
}
61
+
},
62
+
"description": "Allow replies from actors on a list."
63
+
},
64
+
"mentionRule": {
65
+
"type": "object",
66
+
"properties": {},
67
+
"description": "Allow replies from actors mentioned in your post."
68
+
},
69
+
"followerRule": {
70
+
"type": "object",
71
+
"properties": {},
72
+
"description": "Allow replies from actors who follow you."
73
+
},
74
+
"followingRule": {
75
+
"type": "object",
76
+
"properties": {},
77
+
"description": "Allow replies from actors you follow."
78
+
}
79
+
}
80
+
}
+332
lexicons/app/bsky/graph/defs.json
+332
lexicons/app/bsky/graph/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.defs",
4
+
"defs": {
5
+
"modlist": {
6
+
"type": "token",
7
+
"description": "A list of actors to apply an aggregate moderation action (mute/block) on."
8
+
},
9
+
"listView": {
10
+
"type": "object",
11
+
"required": [
12
+
"uri",
13
+
"cid",
14
+
"creator",
15
+
"name",
16
+
"purpose",
17
+
"indexedAt"
18
+
],
19
+
"properties": {
20
+
"cid": {
21
+
"type": "string",
22
+
"format": "cid"
23
+
},
24
+
"uri": {
25
+
"type": "string",
26
+
"format": "at-uri"
27
+
},
28
+
"name": {
29
+
"type": "string",
30
+
"maxLength": 64,
31
+
"minLength": 1
32
+
},
33
+
"avatar": {
34
+
"type": "string",
35
+
"format": "uri"
36
+
},
37
+
"labels": {
38
+
"type": "array",
39
+
"items": {
40
+
"ref": "com.atproto.label.defs#label",
41
+
"type": "ref"
42
+
}
43
+
},
44
+
"viewer": {
45
+
"ref": "#listViewerState",
46
+
"type": "ref"
47
+
},
48
+
"creator": {
49
+
"ref": "app.bsky.actor.defs#profileView",
50
+
"type": "ref"
51
+
},
52
+
"purpose": {
53
+
"ref": "#listPurpose",
54
+
"type": "ref"
55
+
},
56
+
"indexedAt": {
57
+
"type": "string",
58
+
"format": "datetime"
59
+
},
60
+
"description": {
61
+
"type": "string",
62
+
"maxLength": 3000,
63
+
"maxGraphemes": 300
64
+
},
65
+
"listItemCount": {
66
+
"type": "integer",
67
+
"minimum": 0
68
+
},
69
+
"descriptionFacets": {
70
+
"type": "array",
71
+
"items": {
72
+
"ref": "app.bsky.richtext.facet",
73
+
"type": "ref"
74
+
}
75
+
}
76
+
}
77
+
},
78
+
"curatelist": {
79
+
"type": "token",
80
+
"description": "A list of actors used for curation purposes such as list feeds or interaction gating."
81
+
},
82
+
"listPurpose": {
83
+
"type": "string",
84
+
"knownValues": [
85
+
"app.bsky.graph.defs#modlist",
86
+
"app.bsky.graph.defs#curatelist",
87
+
"app.bsky.graph.defs#referencelist"
88
+
]
89
+
},
90
+
"listItemView": {
91
+
"type": "object",
92
+
"required": [
93
+
"uri",
94
+
"subject"
95
+
],
96
+
"properties": {
97
+
"uri": {
98
+
"type": "string",
99
+
"format": "at-uri"
100
+
},
101
+
"subject": {
102
+
"ref": "app.bsky.actor.defs#profileView",
103
+
"type": "ref"
104
+
}
105
+
}
106
+
},
107
+
"relationship": {
108
+
"type": "object",
109
+
"required": [
110
+
"did"
111
+
],
112
+
"properties": {
113
+
"did": {
114
+
"type": "string",
115
+
"format": "did"
116
+
},
117
+
"following": {
118
+
"type": "string",
119
+
"format": "at-uri",
120
+
"description": "if the actor follows this DID, this is the AT-URI of the follow record"
121
+
},
122
+
"followedBy": {
123
+
"type": "string",
124
+
"format": "at-uri",
125
+
"description": "if the actor is followed by this DID, contains the AT-URI of the follow record"
126
+
}
127
+
},
128
+
"description": "lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)"
129
+
},
130
+
"listViewBasic": {
131
+
"type": "object",
132
+
"required": [
133
+
"uri",
134
+
"cid",
135
+
"name",
136
+
"purpose"
137
+
],
138
+
"properties": {
139
+
"cid": {
140
+
"type": "string",
141
+
"format": "cid"
142
+
},
143
+
"uri": {
144
+
"type": "string",
145
+
"format": "at-uri"
146
+
},
147
+
"name": {
148
+
"type": "string",
149
+
"maxLength": 64,
150
+
"minLength": 1
151
+
},
152
+
"avatar": {
153
+
"type": "string",
154
+
"format": "uri"
155
+
},
156
+
"labels": {
157
+
"type": "array",
158
+
"items": {
159
+
"ref": "com.atproto.label.defs#label",
160
+
"type": "ref"
161
+
}
162
+
},
163
+
"viewer": {
164
+
"ref": "#listViewerState",
165
+
"type": "ref"
166
+
},
167
+
"purpose": {
168
+
"ref": "#listPurpose",
169
+
"type": "ref"
170
+
},
171
+
"indexedAt": {
172
+
"type": "string",
173
+
"format": "datetime"
174
+
},
175
+
"listItemCount": {
176
+
"type": "integer",
177
+
"minimum": 0
178
+
}
179
+
}
180
+
},
181
+
"notFoundActor": {
182
+
"type": "object",
183
+
"required": [
184
+
"actor",
185
+
"notFound"
186
+
],
187
+
"properties": {
188
+
"actor": {
189
+
"type": "string",
190
+
"format": "at-identifier"
191
+
},
192
+
"notFound": {
193
+
"type": "boolean",
194
+
"const": true
195
+
}
196
+
},
197
+
"description": "indicates that a handle or DID could not be resolved"
198
+
},
199
+
"referencelist": {
200
+
"type": "token",
201
+
"description": "A list of actors used for only for reference purposes such as within a starter pack."
202
+
},
203
+
"listViewerState": {
204
+
"type": "object",
205
+
"properties": {
206
+
"muted": {
207
+
"type": "boolean"
208
+
},
209
+
"blocked": {
210
+
"type": "string",
211
+
"format": "at-uri"
212
+
}
213
+
}
214
+
},
215
+
"starterPackView": {
216
+
"type": "object",
217
+
"required": [
218
+
"uri",
219
+
"cid",
220
+
"record",
221
+
"creator",
222
+
"indexedAt"
223
+
],
224
+
"properties": {
225
+
"cid": {
226
+
"type": "string",
227
+
"format": "cid"
228
+
},
229
+
"uri": {
230
+
"type": "string",
231
+
"format": "at-uri"
232
+
},
233
+
"list": {
234
+
"ref": "#listViewBasic",
235
+
"type": "ref"
236
+
},
237
+
"feeds": {
238
+
"type": "array",
239
+
"items": {
240
+
"ref": "app.bsky.feed.defs#generatorView",
241
+
"type": "ref"
242
+
},
243
+
"maxLength": 3
244
+
},
245
+
"labels": {
246
+
"type": "array",
247
+
"items": {
248
+
"ref": "com.atproto.label.defs#label",
249
+
"type": "ref"
250
+
}
251
+
},
252
+
"record": {
253
+
"type": "unknown"
254
+
},
255
+
"creator": {
256
+
"ref": "app.bsky.actor.defs#profileViewBasic",
257
+
"type": "ref"
258
+
},
259
+
"indexedAt": {
260
+
"type": "string",
261
+
"format": "datetime"
262
+
},
263
+
"joinedWeekCount": {
264
+
"type": "integer",
265
+
"minimum": 0
266
+
},
267
+
"listItemsSample": {
268
+
"type": "array",
269
+
"items": {
270
+
"ref": "#listItemView",
271
+
"type": "ref"
272
+
},
273
+
"maxLength": 12
274
+
},
275
+
"joinedAllTimeCount": {
276
+
"type": "integer",
277
+
"minimum": 0
278
+
}
279
+
}
280
+
},
281
+
"starterPackViewBasic": {
282
+
"type": "object",
283
+
"required": [
284
+
"uri",
285
+
"cid",
286
+
"record",
287
+
"creator",
288
+
"indexedAt"
289
+
],
290
+
"properties": {
291
+
"cid": {
292
+
"type": "string",
293
+
"format": "cid"
294
+
},
295
+
"uri": {
296
+
"type": "string",
297
+
"format": "at-uri"
298
+
},
299
+
"labels": {
300
+
"type": "array",
301
+
"items": {
302
+
"ref": "com.atproto.label.defs#label",
303
+
"type": "ref"
304
+
}
305
+
},
306
+
"record": {
307
+
"type": "unknown"
308
+
},
309
+
"creator": {
310
+
"ref": "app.bsky.actor.defs#profileViewBasic",
311
+
"type": "ref"
312
+
},
313
+
"indexedAt": {
314
+
"type": "string",
315
+
"format": "datetime"
316
+
},
317
+
"listItemCount": {
318
+
"type": "integer",
319
+
"minimum": 0
320
+
},
321
+
"joinedWeekCount": {
322
+
"type": "integer",
323
+
"minimum": 0
324
+
},
325
+
"joinedAllTimeCount": {
326
+
"type": "integer",
327
+
"minimum": 0
328
+
}
329
+
}
330
+
}
331
+
}
332
+
}
+28
lexicons/app/bsky/graph/follow.json
+28
lexicons/app/bsky/graph/follow.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.graph.follow",
4
+
"defs": {
5
+
"main": {
6
+
"key": "tid",
7
+
"type": "record",
8
+
"record": {
9
+
"type": "object",
10
+
"required": [
11
+
"subject",
12
+
"createdAt"
13
+
],
14
+
"properties": {
15
+
"subject": {
16
+
"type": "string",
17
+
"format": "did"
18
+
},
19
+
"createdAt": {
20
+
"type": "string",
21
+
"format": "datetime"
22
+
}
23
+
}
24
+
},
25
+
"description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView."
26
+
}
27
+
}
28
+
}
+128
lexicons/app/bsky/labeler/defs.json
+128
lexicons/app/bsky/labeler/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.labeler.defs",
4
+
"defs": {
5
+
"labelerView": {
6
+
"type": "object",
7
+
"required": [
8
+
"uri",
9
+
"cid",
10
+
"creator",
11
+
"indexedAt"
12
+
],
13
+
"properties": {
14
+
"cid": {
15
+
"type": "string",
16
+
"format": "cid"
17
+
},
18
+
"uri": {
19
+
"type": "string",
20
+
"format": "at-uri"
21
+
},
22
+
"labels": {
23
+
"type": "array",
24
+
"items": {
25
+
"ref": "com.atproto.label.defs#label",
26
+
"type": "ref"
27
+
}
28
+
},
29
+
"viewer": {
30
+
"ref": "#labelerViewerState",
31
+
"type": "ref"
32
+
},
33
+
"creator": {
34
+
"ref": "app.bsky.actor.defs#profileView",
35
+
"type": "ref"
36
+
},
37
+
"indexedAt": {
38
+
"type": "string",
39
+
"format": "datetime"
40
+
},
41
+
"likeCount": {
42
+
"type": "integer",
43
+
"minimum": 0
44
+
}
45
+
}
46
+
},
47
+
"labelerPolicies": {
48
+
"type": "object",
49
+
"required": [
50
+
"labelValues"
51
+
],
52
+
"properties": {
53
+
"labelValues": {
54
+
"type": "array",
55
+
"items": {
56
+
"ref": "com.atproto.label.defs#labelValue",
57
+
"type": "ref"
58
+
},
59
+
"description": "The label values which this labeler publishes. May include global or custom labels."
60
+
},
61
+
"labelValueDefinitions": {
62
+
"type": "array",
63
+
"items": {
64
+
"ref": "com.atproto.label.defs#labelValueDefinition",
65
+
"type": "ref"
66
+
},
67
+
"description": "Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler."
68
+
}
69
+
}
70
+
},
71
+
"labelerViewerState": {
72
+
"type": "object",
73
+
"properties": {
74
+
"like": {
75
+
"type": "string",
76
+
"format": "at-uri"
77
+
}
78
+
}
79
+
},
80
+
"labelerViewDetailed": {
81
+
"type": "object",
82
+
"required": [
83
+
"uri",
84
+
"cid",
85
+
"creator",
86
+
"policies",
87
+
"indexedAt"
88
+
],
89
+
"properties": {
90
+
"cid": {
91
+
"type": "string",
92
+
"format": "cid"
93
+
},
94
+
"uri": {
95
+
"type": "string",
96
+
"format": "at-uri"
97
+
},
98
+
"labels": {
99
+
"type": "array",
100
+
"items": {
101
+
"ref": "com.atproto.label.defs#label",
102
+
"type": "ref"
103
+
}
104
+
},
105
+
"viewer": {
106
+
"ref": "#labelerViewerState",
107
+
"type": "ref"
108
+
},
109
+
"creator": {
110
+
"ref": "app.bsky.actor.defs#profileView",
111
+
"type": "ref"
112
+
},
113
+
"policies": {
114
+
"ref": "app.bsky.labeler.defs#labelerPolicies",
115
+
"type": "ref"
116
+
},
117
+
"indexedAt": {
118
+
"type": "string",
119
+
"format": "datetime"
120
+
},
121
+
"likeCount": {
122
+
"type": "integer",
123
+
"minimum": 0
124
+
}
125
+
}
126
+
}
127
+
}
128
+
}
+89
lexicons/app/bsky/richtext/facet.json
+89
lexicons/app/bsky/richtext/facet.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.bsky.richtext.facet",
4
+
"defs": {
5
+
"tag": {
6
+
"type": "object",
7
+
"required": [
8
+
"tag"
9
+
],
10
+
"properties": {
11
+
"tag": {
12
+
"type": "string",
13
+
"maxLength": 640,
14
+
"maxGraphemes": 64
15
+
}
16
+
},
17
+
"description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags')."
18
+
},
19
+
"link": {
20
+
"type": "object",
21
+
"required": [
22
+
"uri"
23
+
],
24
+
"properties": {
25
+
"uri": {
26
+
"type": "string",
27
+
"format": "uri"
28
+
}
29
+
},
30
+
"description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL."
31
+
},
32
+
"main": {
33
+
"type": "object",
34
+
"required": [
35
+
"index",
36
+
"features"
37
+
],
38
+
"properties": {
39
+
"index": {
40
+
"ref": "#byteSlice",
41
+
"type": "ref"
42
+
},
43
+
"features": {
44
+
"type": "array",
45
+
"items": {
46
+
"refs": [
47
+
"#mention",
48
+
"#link",
49
+
"#tag"
50
+
],
51
+
"type": "union"
52
+
}
53
+
}
54
+
},
55
+
"description": "Annotation of a sub-string within rich text."
56
+
},
57
+
"mention": {
58
+
"type": "object",
59
+
"required": [
60
+
"did"
61
+
],
62
+
"properties": {
63
+
"did": {
64
+
"type": "string",
65
+
"format": "did"
66
+
}
67
+
},
68
+
"description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID."
69
+
},
70
+
"byteSlice": {
71
+
"type": "object",
72
+
"required": [
73
+
"byteStart",
74
+
"byteEnd"
75
+
],
76
+
"properties": {
77
+
"byteEnd": {
78
+
"type": "integer",
79
+
"minimum": 0
80
+
},
81
+
"byteStart": {
82
+
"type": "integer",
83
+
"minimum": 0
84
+
}
85
+
},
86
+
"description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets."
87
+
}
88
+
}
89
+
}
-22
lexicons/network/slices/waiting.json
-22
lexicons/network/slices/waiting.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "network.slices.waiting",
4
-
"defs": {
5
-
"main": {
6
-
"type": "record",
7
-
"description": "Existence of this record means you're on the waitist",
8
-
"key": "literal:self",
9
-
"record": {
10
-
"type": "object",
11
-
"required": ["createdAt"],
12
-
"properties": {
13
-
"createdAt": {
14
-
"type": "string",
15
-
"format": "datetime",
16
-
"description": "When the user joined the waitlist"
17
-
}
18
-
}
19
-
}
20
-
}
21
-
}
22
-
}
+65
lexicons/network/slices/waitlist/defs.json
+65
lexicons/network/slices/waitlist/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.waitlist.defs",
4
+
"defs": {
5
+
"requestView": {
6
+
"type": "object",
7
+
"description": "A request to join the waitlist with profile information",
8
+
"required": ["slice", "createdAt"],
9
+
"properties": {
10
+
"slice": {
11
+
"type": "string",
12
+
"format": "at-uri",
13
+
"description": "The AT URI of the slice being requested access to"
14
+
},
15
+
"createdAt": {
16
+
"type": "string",
17
+
"format": "datetime",
18
+
"description": "When the user joined the waitlist"
19
+
},
20
+
"profile": {
21
+
"type": "ref",
22
+
"ref": "app.bsky.actor.defs#profileViewBasic",
23
+
"description": "Profile of the requester"
24
+
}
25
+
}
26
+
},
27
+
"inviteView": {
28
+
"type": "object",
29
+
"description": "An invite granting a DID access with profile information",
30
+
"required": ["did", "slice", "createdAt"],
31
+
"properties": {
32
+
"did": {
33
+
"type": "string",
34
+
"format": "did",
35
+
"description": "The DID being invited"
36
+
},
37
+
"slice": {
38
+
"type": "string",
39
+
"format": "at-uri",
40
+
"description": "The AT URI of the slice this invite is for"
41
+
},
42
+
"createdAt": {
43
+
"type": "string",
44
+
"format": "datetime",
45
+
"description": "When this invitation was created"
46
+
},
47
+
"expiresAt": {
48
+
"type": "string",
49
+
"format": "datetime",
50
+
"description": "Optional expiration date for this invitation"
51
+
},
52
+
"uri": {
53
+
"type": "string",
54
+
"format": "at-uri",
55
+
"description": "The AT URI of this invite record"
56
+
},
57
+
"profile": {
58
+
"type": "ref",
59
+
"ref": "app.bsky.actor.defs#profileViewBasic",
60
+
"description": "Profile of the invitee"
61
+
}
62
+
}
63
+
}
64
+
}
65
+
}
+37
lexicons/network/slices/waitlist/invite.json
+37
lexicons/network/slices/waitlist/invite.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.waitlist.invite",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "An invite granting a DID access, created by the slice owner",
8
+
"key": "tid",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["did", "slice", "createdAt"],
12
+
"properties": {
13
+
"did": {
14
+
"type": "string",
15
+
"format": "did",
16
+
"description": "The DID being invited"
17
+
},
18
+
"slice": {
19
+
"type": "string",
20
+
"format": "at-uri",
21
+
"description": "The AT URI of the slice this invite is for"
22
+
},
23
+
"createdAt": {
24
+
"type": "string",
25
+
"format": "datetime",
26
+
"description": "When this invitation was created"
27
+
},
28
+
"expiresAt": {
29
+
"type": "string",
30
+
"format": "datetime",
31
+
"description": "Optional expiration date for this invitation"
32
+
}
33
+
}
34
+
}
35
+
}
36
+
}
37
+
}
+27
lexicons/network/slices/waitlist/request.json
+27
lexicons/network/slices/waitlist/request.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "network.slices.waitlist.request",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "A request to join the waitlist",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": ["slice", "createdAt"],
12
+
"properties": {
13
+
"slice": {
14
+
"type": "string",
15
+
"format": "at-uri",
16
+
"description": "The AT URI of the slice being requested access to"
17
+
},
18
+
"createdAt": {
19
+
"type": "string",
20
+
"format": "datetime",
21
+
"description": "When the user joined the waitlist"
22
+
}
23
+
}
24
+
}
25
+
}
26
+
}
27
+
}
+1
-1
packages/client/deno.json
+1
-1
packages/client/deno.json
+30
-7
packages/client/src/mod.ts
+30
-7
packages/client/src/mod.ts
···
44
44
| "collection"
45
45
| "uri"
46
46
| "cid"
47
-
| "indexedAt";
47
+
| "indexedAt"
48
+
| "json";
48
49
49
50
export interface SortField<TField extends string = string> {
50
51
field: TField;
···
245
246
}
246
247
}
247
248
248
-
throw new Error(
249
-
`Request failed: ${response.status} ${response.statusText}`
250
-
);
249
+
// Try to read the response body for detailed error information
250
+
let errorMessage = `Request failed: ${response.status} ${response.statusText}`;
251
+
try {
252
+
const errorBody = await response.json();
253
+
if (errorBody?.message) {
254
+
errorMessage += ` - ${errorBody.message}`;
255
+
} else if (errorBody?.error) {
256
+
errorMessage += ` - ${errorBody.error}`;
257
+
}
258
+
} catch {
259
+
// If we can't parse the response body, just use the status message
260
+
}
261
+
262
+
throw new Error(errorMessage);
251
263
}
252
264
253
265
return (await response.json()) as T;
···
327
339
}
328
340
}
329
341
330
-
throw new Error(
331
-
`Blob upload failed: ${response.status} ${response.statusText}`
332
-
);
342
+
// Try to read the response body for detailed error information
343
+
let errorMessage = `Blob upload failed: ${response.status} ${response.statusText}`;
344
+
try {
345
+
const errorBody = await response.json();
346
+
if (errorBody?.message) {
347
+
errorMessage += ` - ${errorBody.message}`;
348
+
} else if (errorBody?.error) {
349
+
errorMessage += ` - ${errorBody.error}`;
350
+
}
351
+
} catch {
352
+
// If we can't parse the response body, just use the status message
353
+
}
354
+
355
+
throw new Error(errorMessage);
333
356
}
334
357
335
358
return await response.json();