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 { 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});