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