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.mmatt.net/xrpc/com.bad-example.identity.resolveMiniDoc"
42 );
43 url.searchParams.set("identifier", did);
44 const res = await fetch(url).then((r) => r.json());
45 return { handle: res.handle || null, did };
46 } catch {
47 return null;
48 }
49 })
50 );
51
52 const valid = handles.filter(Boolean);
53 const cols = 3;
54 const gap = " ";
55
56 const lines = [
57 `welcome to teal.town!`,
58 ``,
59 `<a target="_blank" href="https://pdsmoover.com/moover/teal.town">move to town!</a>`,
60 ``,
61 `contact your webmaster:`,
62 ` \\ o /`,
63 ` | <a href="mailto:${email}">${email}</a>`,
64 ` / \\`,
65 ``,
66 ];
67
68 // --- legal stuff (links left, guy right) ---
69 if (links.privacyPolicy || links.termsOfService) {
70 const ppPlain = links.privacyPolicy
71 ? `privacy policy: ${links.privacyPolicy}`
72 : "";
73 const tosPlain = links.termsOfService
74 ? `terms of service: ${links.termsOfService}`
75 : "";
76 const longest = Math.max(ppPlain.length, tosPlain.length);
77 const offset = longest + 10;
78
79 lines.push(`legal stuff:`);
80 if (links.privacyPolicy) {
81 const plain = `privacy policy: ${links.privacyPolicy}`;
82 const padded = plain.padEnd(offset, " ");
83 const html = padded.replace(
84 links.privacyPolicy,
85 `<a href="${links.privacyPolicy}">${links.privacyPolicy}</a>`
86 );
87 lines.push(html + ` \\ o / `);
88 }
89 if (links.termsOfService) {
90 const plain = `terms of service: ${links.termsOfService}`;
91 const padded = plain.padEnd(offset, " ");
92 const html = padded.replace(
93 links.termsOfService,
94 `<a href="${links.termsOfService}">${links.termsOfService}</a>`
95 );
96 lines.push(html + `💼 |`);
97 }
98 lines.push("".padEnd(offset, " ") + ` / \\`);
99 lines.push(``);
100 }
101
102 lines.push(`meet your neighbors:`);
103
104 for (let r = 0; r < valid.length; r += cols) {
105 const chunk = valid.slice(r, r + cols);
106 const strip = [[], [], [], [], []]; // 5 rows: head, arms, legs, handle, icons
107
108 for (const { handle, did } of chunk) {
109 const len = handle.length;
110 const pad = (len - 5) / 2;
111 const p1 = Math.max(0, Math.ceil(pad));
112 const p2 = Math.max(0, Math.floor(pad));
113
114 if (handle === "bailey.teal.town") {
115 strip[0].push(" ".repeat(p1) + "\\ 🤠 /" + " ".repeat(p2));
116 } else {
117 strip[0].push(" ".repeat(p1) + "\\ o /" + " ".repeat(p2));
118 }
119 strip[1].push(" ".repeat(p1 + 2) + "|" + " ".repeat(p2 + 2));
120 strip[2].push(" ".repeat(p1 + 1) + "/ \\" + " ".repeat(p2 + 1));
121 strip[3].push(handle);
122
123 const icons = `<a href="https://bsky.app/profile/${did}">🦋</a> <a href="https://pdsls.dev/at://${did}">📁</a>`;
124 // Use same padding as head row for perfect alignment with ASCII art
125 strip[4].push(" ".repeat(p1) + icons + " ".repeat(p2));
126 }
127
128 for (const row of strip) lines.push(row.join(gap));
129 lines.push(""); // blank line between 3-person rows
130 }
131
132 const pre = document.getElementById("out");
133 pre.innerHTML += lines.join("\n");
134 })();
135 </script>
136 </body>
137</html>