+2
compose.yaml
+2
compose.yaml
+2
-1
landing/Dockerfile
+2
-1
landing/Dockerfile
-66
landing/css.css
-66
landing/css.css
···
1
-
@property --gradient-offset {
2
-
syntax: "<length>";
3
-
inherits: false;
4
-
initial-value: 0px;
5
-
}
6
-
7
-
@keyframes scroll {
8
-
from {
9
-
--gradient-offset: 0px;
10
-
}
11
-
12
-
to {
13
-
--gradient-offset: var(--gradient-gap);
14
-
}
15
-
}
16
-
17
-
html {
18
-
background: repeating-linear-gradient(#fff1, #0000 5px, #fff1 10px), black;
19
-
min-height: 100%;
20
-
font-family: "Monaspace neon var", monospace;
21
-
font-weight: bold;
22
-
}
23
-
24
-
html,
25
-
body {
26
-
margin: 0;
27
-
}
28
-
29
-
pre {
30
-
/* config */
31
-
--light: lime;
32
-
--dark: green;
33
-
--gradient-gap: 10px;
34
-
--gradient-size: 5px;
35
-
--gradient-offset: 0px;
36
-
--gradient-max: calc(var(--gradient-gap) * 2 + var(--gradient-size));
37
-
38
-
/* override some resource://content-accessible/plaintext.css styles in ff */
39
-
white-space: pre !important;
40
-
width: max-content;
41
-
padding: 1em;
42
-
margin-block: 0;
43
-
padding-block-start: 0;
44
-
45
-
text-shadow: 0 0 5px lime;
46
-
color: lime;
47
-
@supports (background-clip: text) {
48
-
color: transparent;
49
-
background: repeating-linear-gradient(
50
-
var(--light),
51
-
var(--light) calc(var(--gradient-gap) - var(--gradient-offset)),
52
-
var(--dark) calc(var(--gradient-gap) - var(--gradient-offset)),
53
-
var(--dark)
54
-
calc(
55
-
var(--gradient-gap) - var(--gradient-offset) + var(--gradient-size)
56
-
),
57
-
var(--light)
58
-
calc(
59
-
var(--gradient-gap) - var(--gradient-offset) + var(--gradient-size)
60
-
),
61
-
var(--light) var(--gradient-max)
62
-
)
63
-
text;
64
-
animation: 10s infinite scroll linear;
65
-
}
66
-
}
+16
landing/deno.lock
+16
landing/deno.lock
···
1
+
{
2
+
"version": "5",
3
+
"specifiers": {
4
+
"npm:zod@^4.1.13": "4.1.13"
5
+
},
6
+
"npm": {
7
+
"zod@4.1.13": {
8
+
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="
9
+
}
10
+
},
11
+
"workspace": {
12
+
"dependencies": [
13
+
"npm:zod@^4.1.13"
14
+
]
15
+
}
16
+
}
+1
landing/heading.txt
+1
landing/heading.txt
+156
landing/index.ts
+156
landing/index.ts
···
1
+
import css from "./styles.css" with { type: "text" };
2
+
import heading from "./heading.txt" with { type: "text" };
3
+
import {
4
+
didDocSchema,
5
+
getRecordSchema,
6
+
listRecordsSchema,
7
+
listReposSchema,
8
+
profileSelfSchema,
9
+
statusphereSchema,
10
+
tealFmStatusSchema,
11
+
} from "./schemas.ts";
12
+
13
+
const PORT = Number(Deno.env.get("PORT"));
14
+
const PDS = Deno.env.get("PDS") ?? "http://pi:8000";
15
+
16
+
async function renderUser(did: string): Promise<string> {
17
+
const handle = fetch(
18
+
did.startsWith("did:plc")
19
+
? "https://plc.directory/" + did
20
+
: `https://${did.replace("did:web:", "")}/.well-known/did.json`
21
+
)
22
+
.then((res) => res.json())
23
+
.then((res) => didDocSchema.safeParse(res).data)
24
+
.then(
25
+
(doc) =>
26
+
doc.alsoKnownAs
27
+
.filter((x) => x.startsWith("at://"))[0]
28
+
?.replace("at://", "") ?? "invalid.handle"
29
+
);
30
+
31
+
const displayName = fetch(
32
+
`${PDS}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.actor.profile&rkey=self`
33
+
)
34
+
.then((res) => res.json())
35
+
.then((data) => getRecordSchema(profileSelfSchema).safeParse(data).data)
36
+
.then((data) => (data ? data.value.displayName : undefined));
37
+
38
+
const statusphere = fetch(
39
+
`${PDS}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=xyz.statusphere.status`
40
+
)
41
+
.then((res) => res.json())
42
+
.then((data) => listRecordsSchema(statusphereSchema).safeParse(data).data)
43
+
.then((data) =>
44
+
data && data.records.length > 0 ? data.records[0].value.status : undefined
45
+
);
46
+
47
+
const nowPlaying = fetch(
48
+
`${PDS}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=fm.teal.alpha.actor.status&rkey=self`
49
+
)
50
+
.then((res) => res.json())
51
+
.then((data) => getRecordSchema(tealFmStatusSchema).safeParse(data).data)
52
+
.then((data) =>
53
+
data
54
+
? data.value.item.trackName +
55
+
((data.value.item.artists.length > 0
56
+
? ` - ${data.value.item.artists[0].artistName}`
57
+
: "") +
58
+
(data.value.item.artists.length > 1
59
+
? `${data.value.item.artists.length - 1} more artist${data.value.item.artists.length > 1 ? "s" : ""}`
60
+
: ""))
61
+
: undefined
62
+
);
63
+
64
+
return (
65
+
"- " +
66
+
[
67
+
(await statusphere)
68
+
? `${await displayName} (${await statusphere})`
69
+
: await displayName,
70
+
71
+
`@${await handle}`,
72
+
`at://${did}`,
73
+
await nowPlaying,
74
+
]
75
+
.filter((x) => x)
76
+
.join("\n ")
77
+
);
78
+
}
79
+
80
+
function landingPage(): Response {
81
+
// get upstream pds status
82
+
const status = fetch(`${PDS}/xrpc/_health`).then(
83
+
async (res) => `${res.status} ${res.statusText} ${await res.text()}`
84
+
);
85
+
86
+
const users = fetch(`${PDS}/xrpc/com.atproto.sync.listRepos`)
87
+
.then((res) => res.json())
88
+
.then((data) => listReposSchema.safeParse(data).data)
89
+
.then((data) =>
90
+
data ? data.repos.filter((x) => x.active).map((x) => x.did) : undefined
91
+
)
92
+
.then(async (users) =>
93
+
users
94
+
? await Promise.all(users.map((did) => renderUser(did))).then((x) =>
95
+
x.join("\n\n")
96
+
)
97
+
: undefined
98
+
);
99
+
100
+
const body = new ReadableStream({
101
+
async start(controller: ReadableStreamDefaultController<string>) {
102
+
controller.enqueue(heading);
103
+
controller.enqueue(await status);
104
+
controller.enqueue("\n\n");
105
+
controller.enqueue(await users);
106
+
controller.close();
107
+
},
108
+
});
109
+
110
+
// buffer the output just to be easier to handle
111
+
const stagger = new TransformStream({
112
+
async transform(
113
+
chunk: string,
114
+
controller: TransformStreamDefaultController<string>
115
+
) {
116
+
for (const part of chunk) {
117
+
controller.enqueue(part);
118
+
await new Promise((res) => setTimeout(res, 10));
119
+
}
120
+
},
121
+
});
122
+
123
+
return new Response(
124
+
body.pipeThrough(stagger).pipeThrough(new TextEncoderStream()),
125
+
{
126
+
headers: {
127
+
"Content-Type": "text/text; charset=utf-8",
128
+
"Access-Control-Allow-Origin": "*",
129
+
Link: "</styles.css>; rel=stylesheet",
130
+
},
131
+
}
132
+
);
133
+
}
134
+
135
+
Deno.serve(
136
+
{ port: !Number.isNaN(PORT) && PORT <= 65535 && PORT > 0 ? PORT : 8000 },
137
+
(req) => {
138
+
const path = new URL(req.url).pathname;
139
+
switch (path) {
140
+
case "/":
141
+
return landingPage();
142
+
case "/styles.css":
143
+
return new Response(css, {
144
+
headers: {
145
+
"Content-Type": "text/css; charset=utf-8",
146
+
"Access-Control-Allow-Origin": "*",
147
+
},
148
+
});
149
+
default:
150
+
return new Response(`404 Not Found: ${path} does not exist`, {
151
+
status: 400,
152
+
statusText: "Not Found",
153
+
});
154
+
}
155
+
}
156
+
);
-127
landing/landing.ts
-127
landing/landing.ts
···
1
-
import css from "./css.css" with { type: "text" };
2
-
import heading from "./heading.txt" with { type: "text" };
3
-
4
-
Deno.serve({ port: 8000 }, (req) => {
5
-
switch (new URL(req.url).pathname) {
6
-
case "/css":
7
-
return new Response(css, {
8
-
headers: {
9
-
"Content-Type": "text/css; charset=utf-8",
10
-
"Access-Control-Allow-Origin": "*",
11
-
},
12
-
});
13
-
case "/": {
14
-
const body = new ReadableStream({
15
-
async start(controller) {
16
-
// get upstream pds status
17
-
const status = fetch("http://pi:8000/xrpc/_health").then(
18
-
async (res) =>
19
-
`${res.status} ${res.statusText}: ${await res.text()}`
20
-
);
21
-
22
-
// get list of users;
23
-
// get from pi since it's more reliable than external
24
-
// not awaited so it runs in background while streaming
25
-
const users = fetch("http://pi:8000/xrpc/com.atproto.sync.listRepos")
26
-
// type cast because no point validating for smthn like this
27
-
// real type has more info; not needed here
28
-
.then((res) => res.json() as Promise<{ repos: { did: string }[] }>)
29
-
.then((res) =>
30
-
// get display name, handle, and did for each user
31
-
res.repos.map((repo) => ({
32
-
display: fetch(
33
-
`http://pi:8000/xrpc/com.atproto.repo.getRecord?repo=${repo.did}&collection=app.bsky.actor.profile&rkey=self`
34
-
)
35
-
.then((res) => res.json())
36
-
.then((profile) => profile?.value?.displayName ?? repo.did),
37
-
// dont validate handles because I'm Lazy + trust myself
38
-
handle: fetch(
39
-
repo.did.startsWith("did:plc")
40
-
? "https://plc.directory/" + repo.did
41
-
: `https://${repo.did.replace("did:web:", "")}/.well-known/did.json`
42
-
)
43
-
.then((res) => res.json())
44
-
.then((doc) => doc.alsoKnownAs[0].replace("at://", "")),
45
-
did: repo.did,
46
-
statusphere: fetch(
47
-
`http://pi:8000/xrpc/com.atproto.repo.listRecords?repo=${repo.did}&collection=xyz.statusphere.status`
48
-
)
49
-
.then(
50
-
(res) =>
51
-
res.json() as Promise<{
52
-
records: {
53
-
cid: string;
54
-
value: {
55
-
status: string;
56
-
createdAt: string;
57
-
};
58
-
}[];
59
-
}>
60
-
)
61
-
.then(({ records }) =>
62
-
records.map((x) => ({
63
-
cid: x.cid,
64
-
value: {
65
-
...x.value,
66
-
createdAt: new Date(x.value.createdAt),
67
-
},
68
-
}))
69
-
)
70
-
.then((x) =>
71
-
x
72
-
.sort(
73
-
(a, b) =>
74
-
(b as unknown as number) - (a as unknown as number)
75
-
)
76
-
.at(0)
77
-
)
78
-
.then((x) => x?.value?.status),
79
-
}))
80
-
)
81
-
.then(async (users) =>
82
-
(
83
-
await Promise.all(
84
-
users.map(
85
-
async (x) =>
86
-
`
87
-
- ${await x.display}
88
-
${await x.handle} ${(await x.statusphere) ? `(${await x.statusphere})` : ""}
89
-
${x.did}`
90
-
)
91
-
)
92
-
).join("\n")
93
-
);
94
-
95
-
for (const char of heading) {
96
-
await new Promise((res) => setTimeout(res, 10));
97
-
controller.enqueue(char);
98
-
}
99
-
100
-
for (const char of "\n/xrpc/_health/ " + (await status) + "\n") {
101
-
await new Promise((res) => setTimeout(res, 10));
102
-
controller.enqueue(char);
103
-
}
104
-
105
-
for (const char of "\nAccounts:" + (await users)) {
106
-
await new Promise((res) => setTimeout(res, 10));
107
-
controller.enqueue(char);
108
-
}
109
-
110
-
controller.close();
111
-
},
112
-
});
113
-
114
-
return new Response(body.pipeThrough(new TextEncoderStream()), {
115
-
headers: {
116
-
"Content-Type": "text/text; charset=utf-8",
117
-
"Access-Control-Allow-Origin": "*",
118
-
Link: "</css>; rel=stylesheet",
119
-
},
120
-
});
121
-
}
122
-
}
123
-
124
-
return new Response("404", {
125
-
status: 404,
126
-
});
127
-
});
+49
landing/schemas.ts
+49
landing/schemas.ts
···
1
+
import { z } from "zod";
2
+
3
+
export const listReposSchema = z.object({
4
+
repos: z.array(z.object({ did: z.string(), active: z.boolean() })),
5
+
});
6
+
7
+
export const didDocSchema = z.object({
8
+
alsoKnownAs: z.array(z.string()),
9
+
});
10
+
11
+
export const profileSelfSchema = z.object({
12
+
$type: z.literal("app.bsky.actor.profile"),
13
+
displayName: z.string().optional(),
14
+
});
15
+
16
+
export const statusphereSchema = z.object({
17
+
$type: z.literal("xyz.statusphere.status"),
18
+
status: z.string(),
19
+
});
20
+
21
+
export const tealFmStatusSchema = z.object({
22
+
time: z.string(),
23
+
item: z.object({
24
+
trackName: z.string(),
25
+
artists: z.array(
26
+
z.object({
27
+
artistName: z.string(),
28
+
})
29
+
),
30
+
}),
31
+
});
32
+
33
+
export function getRecordSchema<T>(record: T) {
34
+
return z.object({
35
+
value: record,
36
+
});
37
+
}
38
+
39
+
export function listRecordsSchema<T>(record: T) {
40
+
return z.object({
41
+
records: z.array(
42
+
z.object({
43
+
uri: z.string(),
44
+
cid: z.string(),
45
+
value: record,
46
+
})
47
+
),
48
+
});
49
+
}
+30
landing/styles.css
+30
landing/styles.css
···
1
+
html {
2
+
background: repeating-linear-gradient(#fff1, #0000 5px, #fff1 10px), black;
3
+
min-height: 100%;
4
+
font-family:
5
+
"Monaspace neon var",
6
+
-moz-fixed,
7
+
monospace;
8
+
font-weight: bold;
9
+
}
10
+
11
+
html,
12
+
body {
13
+
margin: 0;
14
+
}
15
+
16
+
pre {
17
+
/* override some resource://content-accessible/plaintext.css styles in ff */
18
+
white-space: pre !important;
19
+
width: max-content;
20
+
padding: 1em;
21
+
margin-block: 0;
22
+
padding-block-start: 0;
23
+
text-shadow: 0 0 5px lime;
24
+
color: lime;
25
+
26
+
&::selection {
27
+
color: black;
28
+
background-color: lime;
29
+
}
30
+
}