an attempt to make a lightweight, easily self-hostable, scoped bluesky appview
at c97cf44fc673932b2b31f7de020ece50585639b4 238 lines 8.5 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"; 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});