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// ]);
70
71
72
73//console.log("ready to serve");
74Deno.serve(
75 { port: config.indexServer.port },
76 async (req: Request): Promise<Response> => {
77 const url = new URL(req.url);
78 const pathname = url.pathname;
79 const searchParams = searchParamsToJson(url.searchParams);
80
81 if (html && js) {
82 if (pathname === "/" || pathname === "") {
83 return new Response(html, {
84 headers: withCors({ "content-type": "text/html; charset=utf-8" }),
85 });
86 }
87 if (pathname === "/landing-index.js") {
88 return new Response(js, {
89 headers: withCors({
90 "content-type": "application/javascript; charset=utf-8",
91 }),
92 });
93 }
94 } else {
95 if (pathname === "/" || pathname === "") {
96 return new Response(`server is compiling your webpage. loading...`, {
97 headers: withCors({ "content-type": "text/html; charset=utf-8" }),
98 });
99 }
100 }
101 if (pathname === "/app.css") {
102 return new Response(css, {
103 headers: withCors({
104 "content-type": "text/css; charset=utf-8",
105 }),
106 });
107 }
108
109
110 if (pathname === "/.well-known/did.json") {
111 return new Response(
112 JSON.stringify(
113 didDocument(
114 "index",
115 config.indexServer.did,
116 config.indexServer.host,
117 "whatever"
118 )
119 ),
120 {
121 headers: withCors({ "Content-Type": "application/json" }),
122 }
123 );
124 }
125 if (pathname === "/health") {
126 return new Response("OK", {
127 status: 200,
128 headers: withCors({
129 "Content-Type": "text/plain",
130 }),
131 });
132 }
133 if (req.method === "OPTIONS") {
134 return new Response(null, {
135 status: 204,
136 headers: {
137 "Access-Control-Allow-Origin": "*",
138 "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
139 "Access-Control-Allow-Headers": "*",
140 },
141 });
142 }
143 console.log(`request for "${pathname}"`);
144 const constellation = pathname.startsWith("/links");
145
146 if (constellation) {
147 const target = searchParams?.target as string;
148 const safeDid = extractDid(target);
149 const targetserver = genericIndexServer.handlesDid(safeDid);
150 if (targetserver) {
151 return genericIndexServer.constellationAPIHandler(req);
152 } else {
153 return new Response(
154 JSON.stringify({
155 error: "User not found",
156 }),
157 {
158 status: 404,
159 headers: withCors({ "Content-Type": "application/json" }),
160 }
161 );
162 }
163 } else {
164 // indexServerRoutes.has(pathname)
165 return await genericIndexServer.indexServerHandler(req);
166 }
167 }
168);
169
170devWatch("index", ({ js: newjs, html: newhtml, css: newcss }) => {
171 js = newjs;
172 html = newhtml;
173 css = newcss;
174});