some files for the teal.town personal data server.
1<html>
2 <head>
3 <meta name="color-scheme" content="light dark" />
4 </head>
5 <body style="pointer-events: auto">
6 <pre
7 id="out"
8 style="
9 overflow-wrap: break-word;
10 white-space: pre-wrap;
11 pointer-events: auto;
12 margin: 0;
13 font-family: monospace;
14 "
15 ></pre>
16
17 <script>
18 (async () => {
19 const PDS_URL = "teal.town";
20 // --- fetch server info ---
21 const serverInfo = await fetch(
22 `https://${PDS_URL}/xrpc/com.atproto.server.describeServer`,
23 )
24 .then((r) => r.json())
25 .catch(() => ({}));
26
27 const email = serverInfo.contact?.email || "admin@teal.town";
28 const links = serverInfo.links || {};
29
30 // --- fetch & build neighbors ---
31 const listUrl = `https://${PDS_URL}/xrpc/com.atproto.sync.listRepos?limit=9`;
32 const repos = await fetch(listUrl)
33 .then((r) => r.json())
34 .catch(() => ({ repos: [] }));
35 const dids = (repos.repos || []).map((r) => r.did).filter(Boolean);
36
37 const handles = await Promise.all(
38 dids.map(async (did) => {
39 try {
40 const url = new URL(
41 "https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc",
42 );
43 url.searchParams.set("identifier", did);
44 const res = await fetch(url, {
45 headers: {
46 "User-Agent": "teal.town-homepage/1.0",
47 },
48 }).then((r) => r.json());
49 return { handle: res.handle || null, did };
50 } catch {
51 return null;
52 }
53 }),
54 );
55
56 const valid = handles.filter(Boolean);
57 const cols = 3;
58 const gap = " ";
59
60 const lines = [
61 `welcome to teal.town!`,
62 ``,
63 `<a target="_blank" href="https://pdsmoover.com/moover/teal.town">move to town!</a>`,
64 ``,
65 `contact your webmaster:`,
66 ` \\ o /`,
67 ` | <a href="mailto:${email}">${email}</a>`,
68 ` / \\`,
69 ``,
70 ];
71
72 // --- legal stuff (links left, guy right) ---
73 if (links.privacyPolicy || links.termsOfService) {
74 const ppPlain = links.privacyPolicy
75 ? `privacy policy: ${links.privacyPolicy}`
76 : "";
77 const tosPlain = links.termsOfService
78 ? `terms of service: ${links.termsOfService}`
79 : "";
80 const longest = Math.max(ppPlain.length, tosPlain.length);
81 const offset = longest + 10;
82
83 lines.push(`legal stuff:`);
84 if (links.privacyPolicy) {
85 const plain = `privacy policy: ${links.privacyPolicy}`;
86 const padded = plain.padEnd(offset, " ");
87 const html = padded.replace(
88 links.privacyPolicy,
89 `<a href="${links.privacyPolicy}">${links.privacyPolicy}</a>`,
90 );
91 lines.push(html + ` \\ o / `);
92 }
93 if (links.termsOfService) {
94 const plain = `terms of service: ${links.termsOfService}`;
95 const padded = plain.padEnd(offset, " ");
96 const html = padded.replace(
97 links.termsOfService,
98 `<a href="${links.termsOfService}">${links.termsOfService}</a>`,
99 );
100 lines.push(html + `💼 |`);
101 }
102 lines.push("".padEnd(offset, " ") + ` / \\`);
103 lines.push(``);
104 }
105
106 lines.push(`meet your neighbors:`);
107
108 for (let r = 0; r < valid.length; r += cols) {
109 const chunk = valid.slice(r, r + cols);
110 const strip = [[], [], [], [], []]; // 5 rows: head, arms, legs, handle, icons
111
112 for (const { handle, did } of chunk) {
113 const len = handle.length;
114 const pad = (len - 5) / 2;
115 const p1 = Math.max(0, Math.ceil(pad));
116 const p2 = Math.max(0, Math.floor(pad));
117
118 if (handle === "bailey.teal.town") {
119 strip[0].push(" ".repeat(p1) + "\\ 🤠 /" + " ".repeat(p2));
120 } else {
121 strip[0].push(" ".repeat(p1) + "\\ o /" + " ".repeat(p2));
122 }
123 strip[1].push(" ".repeat(p1 + 2) + "|" + " ".repeat(p2 + 2));
124 strip[2].push(" ".repeat(p1 + 1) + "/ \\" + " ".repeat(p2 + 1));
125 strip[3].push(handle);
126
127 const icons = `<a href="https://bsky.app/profile/${did}">🦋</a> <a href="https://pdsls.dev/at://${did}">📁</a>`;
128 // Use same padding as head row for perfect alignment with ASCII art
129 strip[4].push(" ".repeat(p1) + icons + " ".repeat(p2));
130 }
131
132 for (const row of strip) lines.push(row.join(gap));
133 lines.push(""); // blank line between 3-person rows
134 }
135
136 const pre = document.getElementById("out");
137 pre.innerHTML += lines.join("\n");
138 })();
139 </script>
140 </body>
141</html>