tangled
alpha
login
or
join now
isuggest.selfce.st
/
strand
3
fork
atom
alternative tangled frontend (extremely wip)
3
fork
atom
overview
issues
pulls
pipelines
feat: more lexicons and abstractions
serenity
3 weeks ago
e0de3231
6ae320bc
+73
-27
4 changed files
expand all
collapse all
unified
split
src
lib
queries
get-avatar.ts
types
lexicons
app
bsky
actor
profile.ts
sh
tangled
actor
profile.ts
shared
blob.ts
+43
-3
src/lib/queries/get-avatar.ts
···
1
1
import { err, ok, Result } from "@/lib/result";
2
2
+
import {
3
3
+
AppBskyActorProfile,
4
4
+
appBskyActorProfileSchema,
5
5
+
} from "@/lib/types/lexicons/app/bsky/actor/profile";
2
6
import { comAtprotoRepoGetRecordOutputSchema } from "@/lib/types/lexicons/com/atproto/repo/getRecord";
3
3
-
import { shTangledActorProfileSchema } from "@/lib/types/lexicons/sh/tangled/actor/profile";
7
7
+
import {
8
8
+
ShTangledActorProfile,
9
9
+
shTangledActorProfileSchema,
10
10
+
} from "@/lib/types/lexicons/sh/tangled/actor/profile";
4
11
import z from "zod";
5
12
6
13
export const getAvatar = async ({
···
39
46
}: {
40
47
did: string;
41
48
repoUrl: string;
42
42
-
}): Promise<Result<z.infer<typeof shTangledActorProfileSchema>, unknown>> => {
49
49
+
}): Promise<Result<ShTangledActorProfile, unknown>> => {
43
50
const req = new Request(
44
51
`${repoUrl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=sh.tangled.actor.profile&rkey=self`,
45
52
);
···
60
67
61
68
if (!tangledProfile.avatar)
62
69
return err(
63
63
-
"Avatar field on Tangled profile record. You should fall back to Bluesky's records.",
70
70
+
"No `avatar` field on Tangled profile record. You should fall back to Bluesky's records.",
64
71
);
65
72
66
73
return ok(tangledProfile);
67
74
};
75
75
+
76
76
+
const getBskyAvatar = async ({
77
77
+
did,
78
78
+
repoUrl,
79
79
+
}: {
80
80
+
did: string;
81
81
+
repoUrl: string;
82
82
+
}): Promise<Result<AppBskyActorProfile, unknown>> => {
83
83
+
const req = new Request(
84
84
+
`${repoUrl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.actor.profile&rkey=self`,
85
85
+
);
86
86
+
const res = await fetch(req);
87
87
+
const data: unknown = await res.json();
88
88
+
89
89
+
const {
90
90
+
success,
91
91
+
error,
92
92
+
data: parseData,
93
93
+
} = comAtprotoRepoGetRecordOutputSchema(
94
94
+
appBskyActorProfileSchema,
95
95
+
).safeParse(data);
96
96
+
97
97
+
if (!success) return err(error);
98
98
+
99
99
+
const { value: bskyProfile } = parseData;
100
100
+
101
101
+
if (!bskyProfile.avatar)
102
102
+
return err(
103
103
+
"No `avatar` field on Bluesky profile record. Profile likely has no avatar on Bluesky.",
104
104
+
);
105
105
+
106
106
+
return ok(bskyProfile);
107
107
+
};
+17
src/lib/types/lexicons/app/bsky/actor/profile.ts
···
1
1
+
import { blobSchema } from "@/lib/types/lexicons/shared/blob";
2
2
+
import z from "zod";
3
3
+
4
4
+
export const appBskyActorProfileSchema = z.object({
5
5
+
displayName: z.string().max(640).optional(),
6
6
+
description: z.string().max(2560).optional(),
7
7
+
pronouns: z.string().max(200).optional(),
8
8
+
website: z.url().optional(),
9
9
+
avatar: blobSchema.optional(),
10
10
+
banner: blobSchema.optional(),
11
11
+
labels: z.unknown().optional(),
12
12
+
joinedViaStarterPack: z.unknown().optional(),
13
13
+
pinnedPost: z.unknown().optional(),
14
14
+
createdAt: z.iso.datetime().optional(),
15
15
+
});
16
16
+
17
17
+
export type AppBskyActorProfile = z.infer<typeof appBskyActorProfileSchema>;
+3
-24
src/lib/types/lexicons/sh/tangled/actor/profile.ts
···
1
1
+
import { blobSchema } from "@/lib/types/lexicons/shared/blob";
1
2
import { z } from "zod/v4";
2
3
3
3
-
/**
4
4
-
* Zod v4 schema for the ATProto lexicon: sh.tangled.actor.profile
5
5
-
*
6
6
-
* A declaration of a Tangled account profile.
7
7
-
* Record key: "literal:self"
8
8
-
*/
9
9
-
10
10
-
const blobSchema = z.object({
11
11
-
$type: z.literal("blob"),
12
12
-
ref: z.object({
13
13
-
$link: z.string(),
14
14
-
}),
15
15
-
mimeType: z.enum(["image/png", "image/jpeg"]),
16
16
-
size: z.number().int().max(1000000),
17
17
-
});
18
18
-
19
4
const _statsEnum = z.enum([
20
5
"merged-pull-request-count",
21
6
"closed-pull-request-count",
···
28
13
29
14
export const shTangledActorProfileSchema = z.object({
30
15
$type: z.literal("sh.tangled.actor.profile"),
31
31
-
32
16
avatar: blobSchema
33
17
.describe(
34
18
"Small image to be displayed next to posts from account. AKA, 'profile picture'",
35
19
)
36
20
.optional(),
37
37
-
38
21
description: z
39
22
.string()
40
23
.max(2560)
41
24
.describe("Free-form profile description text.")
42
25
.optional(),
43
43
-
44
26
links: z
45
27
.array(
46
28
z
···
52
34
.min(0)
53
35
.max(5)
54
36
.optional(),
55
55
-
56
37
stats: z
57
38
.array(_statsEnum.describe("Vanity stats."))
58
39
.min(0)
59
40
.max(2)
60
41
.optional(),
61
61
-
62
42
bluesky: z.boolean().describe("Include link to this account on Bluesky."),
63
63
-
64
43
location: z
65
44
.string()
66
45
.max(400)
67
46
.describe("Free-form location text.")
68
47
.optional(),
69
69
-
70
48
pinnedRepositories: z
71
49
.array(z.string().describe("AT-URI reference to a repository."))
72
50
.min(0)
73
51
.max(6)
74
52
.describe("Any ATURI, it is up to appviews to validate these fields.")
75
53
.optional(),
76
76
-
77
54
pronouns: z
78
55
.string()
79
56
.max(40)
80
57
.describe("Preferred gender pronouns.")
81
58
.optional(),
82
59
});
60
60
+
61
61
+
export type ShTangledActorProfile = z.infer<typeof shTangledActorProfileSchema>;
+10
src/lib/types/lexicons/shared/blob.ts
···
1
1
+
import { z } from "zod/v4";
2
2
+
3
3
+
export const blobSchema = z.object({
4
4
+
$type: z.literal("blob"),
5
5
+
ref: z.object({
6
6
+
$link: z.string(),
7
7
+
}),
8
8
+
mimeType: z.enum(["image/png", "image/jpeg"]),
9
9
+
size: z.number().int().max(1000000),
10
10
+
});