an attempt to make a lightweight, easily self-hostable, scoped bluesky appview
1import { setupAuth, getAuthenticatedDid, authVerifier } from "./utils/auth.ts";
2import { setupSystemDb } from "./utils/dbsystem.ts";
3import { didDocument } from "./utils/diddoc.ts";
4import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts";
5import { IndexServer, IndexServerConfig } from "./indexserver.ts";
6import { extractDid } from "./utils/identity.ts";
7import { config } from "./config.ts";
8import { compile, devWatch } from "./shared-landing/build.ts";
9// ------------------------------------------
10// AppView Setup
11// ------------------------------------------
12
13const indexServerConfig: IndexServerConfig = {
14 baseDbPath: "./dbs/index/registered-users", // The directory for user databases
15 systemDbPath: "./dbs/index/registered-users/system.db", // The path for the main system database
16};
17export const genericIndexServer = new IndexServer(indexServerConfig);
18setupSystemDb(genericIndexServer.systemDB);
19
20let { js, html, css } = await compile({
21 target: "index",
22 initialData: {
23 config: config.indexServer,
24 users: (await genericIndexServer.unspeccedGetRegisteredUsers()) ?? [],
25 },
26});
27
28// add me lol
29genericIndexServer.systemDB.exec(`
30 INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus)
31 VALUES (
32 'did:plc:mn45tewwnse5btfftvd3powc',
33 'admin',
34 datetime('now'),
35 'ready'
36 );
37
38 INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus)
39 VALUES (
40 'did:web:did12.whey.party',
41 'admin',
42 datetime('now'),
43 'ready'
44 );
45`);
46
47genericIndexServer.start();
48
49// ------------------------------------------
50// XRPC Method Implementations
51// ------------------------------------------
52
53// const indexServerRoutes = new Set([
54// "/xrpc/app.bsky.actor.getProfile",
55// "/xrpc/app.bsky.actor.getProfiles",
56// "/xrpc/app.bsky.feed.getActorFeeds",
57// "/xrpc/app.bsky.feed.getFeedGenerator",
58// "/xrpc/app.bsky.feed.getFeedGenerators",
59// "/xrpc/app.bsky.feed.getPosts",
60// "/xrpc/party.whey.app.bsky.feed.getActorLikesPartial",
61// "/xrpc/party.whey.app.bsky.feed.getAuthorFeedPartial",
62// "/xrpc/party.whey.app.bsky.feed.getLikesPartial",
63// "/xrpc/party.whey.app.bsky.feed.getPostThreadPartial",
64// "/xrpc/party.whey.app.bsky.feed.getQuotesPartial",
65// "/xrpc/party.whey.app.bsky.feed.getRepostedByPartial",
66// // more federated endpoints, not planned yet, lexicons will come later
67// /*
68// app.bsky.graph.getLists // doesnt need to because theres no items[], and its self ProfileViewBasic
69// app.bsky.graph.getList // needs to be Partial-ed (items[] union with ProfileViewRef)
70// app.bsky.graph.getActorStarterPacks // maybe doesnt need to be Partial-ed because its self ProfileViewBasic
71
72// app.bsky.feed.getListFeed // uhh actually already exists its getListFeedPartial
73// */
74// "/xrpc/party.whey.app.bsky.feed.getListFeedPartial",
75// ]);
76const placeholderselfcheckstatus = {
77 "#skylite_index:/xrpc/app.bsky.actor.getProfile": "green",
78 "#skylite_index:/xrpc/app.bsky.actor.getProfiles": "green",
79 "#skylite_index:/xrpc/app.bsky.feed.getActorFeeds": "green",
80 "#skylite_index:/xrpc/app.bsky.feed.getFeedGenerator": "green",
81 "#skylite_index:/xrpc/app.bsky.feed.getFeedGenerators": "green",
82 "#skylite_index:/xrpc/app.bsky.feed.getPosts": "green",
83 "#skylite_index:/xrpc/app.bsky.graph.getLists": "black",
84 "#skylite_index:/xrpc/app.bsky.graph.getList": "black",
85 "#skylite_index:/xrpc/app.bsky.graph.getActorStarterPacks": "black",
86 "#skylite_index:/xrpc/party.whey.app.bsky.feed.getActorLikesPartial": "green",
87 "#skylite_index:/xrpc/party.whey.app.bsky.feed.getAuthorFeedPartial": "green",
88 "#skylite_index:/xrpc/party.whey.app.bsky.feed.getLikesPartial": "orange",
89 "#skylite_index:/xrpc/party.whey.app.bsky.feed.getPostThreadPartial": "green",
90 "#skylite_index:/xrpc/party.whey.app.bsky.feed.getQuotesPartial": "orange",
91 "#skylite_index:/xrpc/party.whey.app.bsky.feed.getRepostedByPartial":
92 "orange",
93 "#skylite_index:/xrpc/party.whey.app.bsky.feed.getListFeedPartial": "black",
94
95 "constellation:/links": "green",
96 "constellation:/links/distinct-dids": "green",
97 "constellation:/links/count": "green",
98 "constellation:/links/count/distinct-dids": "green",
99 "constellation:/links/all": "green",
100};
101
102//console.log("ready to serve");
103Deno.serve(
104 { port: config.indexServer.port },
105 async (req: Request): Promise<Response> => {
106 const url = new URL(req.url);
107 const pathname = url.pathname;
108 const searchParams = searchParamsToJson(url.searchParams);
109
110 const publicdir = "/public";
111 if (pathname.startsWith(publicdir)) {
112 const filepath = decodeURIComponent(pathname.slice(publicdir.length));
113 try {
114 const file = await Deno.open("./public" + filepath, { read: true });
115 return new Response(file.readable);
116 } catch {
117 return new Response("404 Not Found", { status: 404 });
118 }
119 }
120
121 const todopleasespecthis = "/_unspecced";
122 if (pathname.startsWith(todopleasespecthis)) {
123 const unspeccedroute = decodeURIComponent(
124 pathname.slice(todopleasespecthis.length)
125 );
126 if (unspeccedroute === "/config") {
127 const safeconfig = {
128 inviteOnly: config.indexServer.inviteOnly,
129 //port: number,
130 did: config.indexServer.did,
131 host: config.indexServer.host,
132 };
133 return new Response(JSON.stringify(safeconfig), {
134 headers: withCors({
135 "content-type": "application/json; charset=utf-8",
136 }),
137 });
138 }
139 if (unspeccedroute === "/users") {
140 const res = await genericIndexServer.unspeccedGetRegisteredUsers();
141 return new Response(JSON.stringify(res), {
142 headers: withCors({
143 "content-type": "application/json; charset=utf-8",
144 }),
145 });
146 }
147 if (unspeccedroute === "/apitest") {
148 return new Response(JSON.stringify(placeholderselfcheckstatus), {
149 headers: withCors({
150 "content-type": "application/json; charset=utf-8",
151 }),
152 });
153 }
154 }
155
156 if (html && js) {
157 if (pathname === "/" || pathname === "") {
158 return new Response(html, {
159 headers: withCors({ "content-type": "text/html; charset=utf-8" }),
160 });
161 }
162 if (pathname === "/landing-index.js") {
163 return new Response(js, {
164 headers: withCors({
165 "content-type": "application/javascript; charset=utf-8",
166 }),
167 });
168 }
169 } else {
170 if (pathname === "/" || pathname === "") {
171 return new Response(`server is compiling your webpage. loading...`, {
172 headers: withCors({ "content-type": "text/html; charset=utf-8" }),
173 });
174 }
175 }
176 if (pathname === "/app.css") {
177 return new Response(css, {
178 headers: withCors({
179 "content-type": "text/css; charset=utf-8",
180 }),
181 });
182 }
183
184 if (pathname === "/.well-known/did.json") {
185 return new Response(
186 JSON.stringify(
187 didDocument(
188 "index",
189 config.indexServer.did,
190 config.indexServer.host,
191 "whatever"
192 )
193 ),
194 {
195 headers: withCors({ "Content-Type": "application/json" }),
196 }
197 );
198 }
199 if (pathname === "/health") {
200 return new Response("OK", {
201 status: 200,
202 headers: withCors({
203 "Content-Type": "text/plain",
204 }),
205 });
206 }
207 if (req.method === "OPTIONS") {
208 return new Response(null, {
209 status: 204,
210 headers: {
211 "Access-Control-Allow-Origin": "*",
212 "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
213 "Access-Control-Allow-Headers": "*",
214 },
215 });
216 }
217 console.log(`request for "${pathname}"`);
218 const constellation = pathname.startsWith("/links");
219
220 if (constellation) {
221 const target = searchParams?.target as string;
222 const safeDid = extractDid(target);
223 const targetserver = genericIndexServer.handlesDid(safeDid);
224 if (targetserver) {
225 return genericIndexServer.constellationAPIHandler(req);
226 } else {
227 return new Response(
228 JSON.stringify({
229 error: "User not found",
230 }),
231 {
232 status: 404,
233 headers: withCors({ "Content-Type": "application/json" }),
234 }
235 );
236 }
237 } else {
238 // indexServerRoutes.has(pathname)
239 return await genericIndexServer.indexServerHandler(req);
240 }
241 }
242);
243
244devWatch({
245 target: "index",
246 initialData: {
247 config: config.indexServer,
248 users: (await genericIndexServer.unspeccedGetRegisteredUsers()) ?? [],
249 },
250 onBuild: ({ js: newjs, html: newhtml, css: newcss }) => {
251 js = newjs;
252 html = newhtml;
253 css = newcss;
254 },
255});