an attempt to make a lightweight, easily self-hostable, scoped bluesky appview
at main 9.4 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 { ViewServer, ViewServerConfig } from "./viewserver.ts"; 6import { extractDid } from "./utils/identity.ts"; 7import { config } from "./config.ts"; 8import { compile, devWatch } from "./shared-landing/build.ts"; 9 10// ------------------------------------------ 11// AppView Setup 12// ------------------------------------------ 13 14setupAuth({ 15 serviceDid: config.viewServer.did, 16 //keyCacheSize: 500, 17 //keyCacheTTL: 10 * 60 * 1000, 18}); 19 20const viewServerConfig: ViewServerConfig = { 21 baseDbPath: "./dbs/view/registered-users", // The directory for user databases 22 systemDbPath: "./dbs/view/registered-users/system.db", // The path for the main system database 23}; 24export const genericViewServer = new ViewServer(viewServerConfig); 25setupSystemDb(genericViewServer.systemDB); 26let { js, html, css } = await compile({ 27 target: "view", 28 initialData: { 29 config: config.viewServer, 30 users: (await genericViewServer.unspeccedGetRegisteredUsers()) ?? [], 31 }, 32}); 33 34// add me lol 35genericViewServer.systemDB.exec(` 36 INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) 37 VALUES ( 38 'did:plc:mn45tewwnse5btfftvd3powc', 39 'admin', 40 datetime('now'), 41 'ready' 42 ); 43 44 INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) 45 VALUES ( 46 'did:web:did12.whey.party', 47 'admin', 48 datetime('now'), 49 'ready' 50 ); 51`); 52 53genericViewServer.start(); 54 55// ------------------------------------------ 56// XRPC Method Implementations 57// ------------------------------------------ 58 59const placeholderselfcheckstatus = { 60 "#bsky_appview:/xrpc/app.bsky.actor.getPreferences": "black", 61 "#bsky_appview:/xrpc/app.bsky.actor.getProfile": "green", 62 "#bsky_appview:/xrpc/app.bsky.actor.getProfiles": "green", 63 "#bsky_appview:/xrpc/app.bsky.actor.getSuggestions": "black", 64 "#bsky_appview:/xrpc/app.bsky.actor.putPreferences": "black", 65 "#bsky_appview:/xrpc/app.bsky.actor.searchActorsTypeahead": "black", 66 "#bsky_appview:/xrpc/app.bsky.actor.searchActors": "black", 67 "#bsky_appview:/xrpc/app.bsky.feed.describeFeedGenerator": "black", 68 "#bsky_appview:/xrpc/app.bsky.feed.getActorFeeds": "black", 69 "#bsky_appview:/xrpc/app.bsky.feed.getActorLikes": "black", 70 "#bsky_appview:/xrpc/app.bsky.feed.getAuthorFeed": "red", 71 "#bsky_appview:/xrpc/app.bsky.feed.getFeedGenerator": "black", 72 "#bsky_appview:/xrpc/app.bsky.feed.getFeedGenerators": "green", // arguably 73 "#bsky_appview:/xrpc/app.bsky.feed.getFeedSkeleton": "black", 74 "#bsky_appview:/xrpc/app.bsky.feed.getFeed": "red", 75 "#bsky_appview:/xrpc/app.bsky.feed.getLikes": "black", 76 "#bsky_appview:/xrpc/app.bsky.feed.getListFeed": "black", 77 "#bsky_appview:/xrpc/app.bsky.feed.getPostThread": "red", 78 "#bsky_appview:/xrpc/app.bsky.unspecced.getPostThreadV2": "red", 79 "#bsky_appview:/xrpc/app.bsky.feed.getPosts": "green", 80 "#bsky_appview:/xrpc/app.bsky.feed.getQuotes": "black", 81 "#bsky_appview:/xrpc/app.bsky.feed.getRepostedBy": "black", 82 "#bsky_appview:/xrpc/app.bsky.feed.getSuggestedFeeds": "black", 83 "#bsky_appview:/xrpc/app.bsky.feed.getTimeline": "black", 84 "#bsky_appview:/xrpc/app.bsky.feed.searchPosts": "black", 85 "#bsky_appview:/xrpc/app.bsky.feed.sendInteractions": "black", 86 "#bsky_appview:/xrpc/app.bsky.graph.getActorStarterPacks": "black", 87 "#bsky_appview:/xrpc/app.bsky.graph.getBlocks": "black", 88 "#bsky_appview:/xrpc/app.bsky.graph.getFollowers": "black", 89 "#bsky_appview:/xrpc/app.bsky.graph.getFollows": "black", 90 "#bsky_appview:/xrpc/app.bsky.graph.getKnownFollowers": "black", 91 "#bsky_appview:/xrpc/app.bsky.graph.getListBlocks": "black", 92 "#bsky_appview:/xrpc/app.bsky.graph.getListMutes": "black", 93 "#bsky_appview:/xrpc/app.bsky.graph.getList": "black", 94 "#bsky_appview:/xrpc/app.bsky.graph.getLists": "black", 95 "#bsky_appview:/xrpc/app.bsky.graph.getMutes": "black", 96 "#bsky_appview:/xrpc/app.bsky.graph.getRelationships": "black", 97 "#bsky_appview:/xrpc/app.bsky.graph.getStarterPack": "black", 98 "#bsky_appview:/xrpc/app.bsky.graph.getStarterPacks": "black", 99 "#bsky_appview:/xrpc/app.bsky.graph.getSuggestedFollowsByActor": "black", 100 "#bsky_appview:/xrpc/app.bsky.graph.muteActorList": "black", 101 "#bsky_appview:/xrpc/app.bsky.graph.muteActor": "black", 102 "#bsky_appview:/xrpc/app.bsky.graph.muteThread": "black", 103 "#bsky_appview:/xrpc/app.bsky.graph.searchStarterPacks": "black", 104 "#bsky_appview:/xrpc/app.bsky.graph.unmuteActorList": "black", 105 "#bsky_appview:/xrpc/app.bsky.graph.unmuteActor": "black", 106 "#bsky_appview:/xrpc/app.bsky.graph.unmuteThread": "black", 107 "#bsky_appview:/xrpc/app.bsky.labeler.getServices": "black", 108 "#bsky_appview:/xrpc/app.bsky.notification.getUnreadCount": "black", 109 "#bsky_appview:/xrpc/app.bsky.notification.listNotifications": "green", 110 "#bsky_appview:/xrpc/app.bsky.notification.putPreferences": "black", 111 "#bsky_appview:/xrpc/app.bsky.notification.registerPush": "black", 112 "#bsky_appview:/xrpc/app.bsky.notification.updateSeen": "black", 113 "#bsky_appview:/xrpc/app.bsky.video.getJobStatus": "black", 114 "#bsky_appview:/xrpc/app.bsky.video.getUploadLimits": "black", 115 "#bsky_appview:/xrpc/app.bsky.video.uploadVideo": "black", 116 "#bsky_appview:/xrpc/app.bsky.unspecced.getTrendingTopics": "red", 117 "#bsky_appview:/xrpc/app.bsky.unspecced.getConfig": "red", 118}; 119 120Deno.serve( 121 { port: config.viewServer.port }, 122 async (req: Request): Promise<Response> => { 123 const url = new URL(req.url); 124 const pathname = url.pathname; 125 const searchParams = searchParamsToJson(url.searchParams); 126 127 const publicdir = "/public"; 128 if (pathname.startsWith(publicdir)) { 129 const filepath = decodeURIComponent(pathname.slice(publicdir.length)); 130 try { 131 const file = await Deno.open("." + filepath, { read: true }); 132 return new Response(file.readable); 133 } catch { 134 return new Response("404 Not Found", { status: 404 }); 135 } 136 } 137 138 const todopleasespecthis = "/_unspecced"; 139 if (pathname.startsWith(todopleasespecthis)) { 140 const unspeccedroute = decodeURIComponent( 141 pathname.slice(todopleasespecthis.length) 142 ); 143 if (unspeccedroute === "/config") { 144 const safeconfig = { 145 inviteOnly: config.viewServer.inviteOnly, 146 //port: number, 147 did: config.viewServer.did, 148 host: config.viewServer.host, 149 indexPriority: config.viewServer.indexPriority, 150 }; 151 return new Response(JSON.stringify(safeconfig), { 152 headers: withCors({ 153 "content-type": "application/json; charset=utf-8", 154 }), 155 }); 156 } 157 if (unspeccedroute === "/users") { 158 const res = await genericViewServer.unspeccedGetRegisteredUsers(); 159 return new Response(JSON.stringify(res), { 160 headers: withCors({ 161 "content-type": "application/json; charset=utf-8", 162 }), 163 }); 164 } 165 if (unspeccedroute === "/apitest") { 166 return new Response(JSON.stringify(placeholderselfcheckstatus), { 167 headers: withCors({ 168 "content-type": "application/json; charset=utf-8", 169 }), 170 }); 171 } 172 } 173 174 if (html && js) { 175 if (pathname === "/" || pathname === "") { 176 return new Response(html, { 177 headers: withCors({ "content-type": "text/html; charset=utf-8" }), 178 }); 179 } 180 if (pathname === "/landing-view.js") { 181 return new Response(js, { 182 headers: withCors({ 183 "content-type": "application/javascript; charset=utf-8", 184 }), 185 }); 186 } 187 } else { 188 if (pathname === "/" || pathname === "") { 189 return new Response(`server is compiling your webpage. loading...`, { 190 headers: withCors({ "content-type": "text/html; charset=utf-8" }), 191 }); 192 } 193 } 194 if (pathname === "/app.css") { 195 return new Response(css, { 196 headers: withCors({ 197 "content-type": "text/css; charset=utf-8", 198 }), 199 }); 200 } 201 202 if (pathname === "/.well-known/did.json") { 203 return new Response( 204 JSON.stringify( 205 didDocument( 206 "view", 207 config.viewServer.did, 208 config.viewServer.host, 209 "whatever" 210 ) 211 ), 212 { 213 headers: withCors({ "Content-Type": "application/json" }), 214 } 215 ); 216 } 217 if (pathname === "/health") { 218 return new Response("OK", { 219 status: 200, 220 headers: withCors({ 221 "Content-Type": "text/plain", 222 }), 223 }); 224 } 225 if (req.method === "OPTIONS") { 226 return new Response(null, { 227 status: 204, 228 headers: { 229 "Access-Control-Allow-Origin": "*", 230 "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 231 "Access-Control-Allow-Headers": "*", 232 }, 233 }); 234 } 235 console.log(`request for "${pathname}"`); 236 return await genericViewServer.viewServerHandler(req); 237 } 238); 239 240devWatch({ 241 target: "view", 242 initialData: { 243 config: config.viewServer, 244 users: await genericViewServer.unspeccedGetRegisteredUsers() ?? [], 245 }, 246 onBuild: ({ js: newjs, html: newhtml, css: newcss }) => { 247 js = newjs; 248 html = newhtml; 249 css = newcss; 250 }, 251});