an attempt to make a lightweight, easily self-hostable, scoped bluesky appview
at main 255 lines 8.9 kB view raw
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});