the statusphere demo reworked into a vite/react app in a monorepo

Merge pull request #8 from bluesky-social/paul/further-repo-cleanup

Further repo cleanup

authored by

Paul Frazee and committed by
GitHub
74dc0f96 e9b9ead1

+69 -22
+1 -1
src/auth/client.ts
··· 1 1 import { NodeOAuthClient } from '@atproto/oauth-client-node' 2 2 import type { Database } from '#/db' 3 - import { env } from '#/env' 3 + import { env } from '#/lib/env' 4 4 import { SessionStore, StateStore } from './storage' 5 5 6 6 export const createClient = async (db: Database) => {
+1 -1
src/auth/session.ts
··· 1 1 import assert from 'node:assert' 2 2 import type { IncomingMessage, ServerResponse } from 'node:http' 3 3 import { getIronSession } from 'iron-session' 4 - import { env } from '#/env' 4 + import { env } from '#/lib/env' 5 5 import { AppContext } from '#/index' 6 6 7 7 export type Session = { did: string }
src/env.ts src/lib/env.ts
+6 -1
src/firehose/firehose.ts
··· 57 57 if (isCommit(evt) && !this.opts.excludeCommit) { 58 58 const parsed = await parseCommit(evt) 59 59 for (const write of parsed) { 60 - if (!this.opts.filterCollections || this.opts.filterCollections.includes(write.uri.collection)) { 60 + if ( 61 + !this.opts.filterCollections || 62 + this.opts.filterCollections.includes(write.uri.collection) 63 + ) { 61 64 yield write 62 65 } 63 66 } ··· 167 170 168 171 type Update = CommitMeta & { 169 172 event: 'update' 173 + record: RepoRecord 174 + cid: CID 170 175 } 171 176 172 177 type Delete = CommitMeta & {
+12 -2
src/firehose/ingester.ts
··· 10 10 const firehose = new Firehose({}) 11 11 12 12 for await (const evt of firehose.run()) { 13 - if (evt.event === 'create') { 13 + // Watch for write events 14 + if (evt.event === 'create' || evt.event === 'update') { 14 15 const record = evt.record 16 + 17 + // If the write is a valid status update 15 18 if ( 16 19 evt.collection === 'com.example.status' && 17 20 Status.isRecord(record) && 18 21 Status.validateRecord(record).success 19 22 ) { 23 + // Store the status in our SQLite 20 24 await this.db 21 25 .insertInto('status') 22 26 .values({ ··· 25 29 updatedAt: record.updatedAt, 26 30 indexedAt: new Date().toISOString(), 27 31 }) 28 - .onConflict((oc) => oc.doNothing()) 32 + .onConflict((oc) => 33 + oc.column('authorDid').doUpdateSet({ 34 + status: record.status, 35 + updatedAt: record.updatedAt, 36 + indexedAt: new Date().toISOString(), 37 + }) 38 + ) 29 39 .execute() 30 40 } 31 41 }
+14 -12
src/index.ts
··· 5 5 import type { OAuthClient } from '@atproto/oauth-client-node' 6 6 7 7 import { createDb, migrateToLatest } from '#/db' 8 - import { env } from '#/env' 8 + import { env } from '#/lib/env' 9 9 import { Ingester } from '#/firehose/ingester' 10 10 import { createRouter } from '#/routes' 11 11 import { createClient } from '#/auth/client' 12 12 import { createResolver, Resolver } from '#/firehose/resolver' 13 13 import type { Database } from '#/db' 14 14 15 + // Application state passed to the router and elsewhere 15 16 export type AppContext = { 16 17 db: Database 17 18 ingester: Ingester ··· 29 30 30 31 static async create() { 31 32 const { NODE_ENV, HOST, PORT, DB_PATH } = env 32 - 33 33 const logger = pino({ name: 'server start' }) 34 + 35 + // Set up the SQLite database 34 36 const db = createDb(DB_PATH) 35 37 await migrateToLatest(db) 36 - const ingester = new Ingester(db) 38 + 39 + // Create the atproto utilities 37 40 const oauthClient = await createClient(db) 41 + const ingester = new Ingester(db) 38 42 const resolver = createResolver() 39 - ingester.start() 40 43 const ctx = { 41 44 db, 42 45 ingester, ··· 45 48 resolver, 46 49 } 47 50 48 - const app: Express = express() 51 + // Subscribe to events on the firehose 52 + ingester.start() 49 53 50 - // Set the application to trust the reverse proxy 54 + // Create our server 55 + const app: Express = express() 51 56 app.set('trust proxy', true) 52 57 53 - // Middlewares 58 + // Routes & middlewares 59 + const router = createRouter(ctx) 54 60 app.use(express.json()) 55 61 app.use(express.urlencoded({ extended: true })) 56 - 57 - // Routes 58 - const router = createRouter(ctx) 59 62 app.use(router) 60 - 61 - // Error handlers 62 63 app.use((_req, res) => res.sendStatus(404)) 63 64 65 + // Bind our server to the port 64 66 const server = app.listen(env.PORT) 65 67 await events.once(server, 'listening') 66 68 logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`)
+1 -2
src/pages/home.ts
··· 1 - import { AtUri } from '@atproto/syntax' 2 1 import type { Status } from '#/db/schema' 3 - import { html } from '../view' 2 + import { html } from '../lib/view' 4 3 import { shell } from './shell' 5 4 6 5 const TODAY = new Date().toDateString()
+1 -1
src/pages/login.ts
··· 1 - import { html } from '../view' 1 + import { html } from '../lib/view' 2 2 import { shell } from './shell' 3 3 4 4 type Props = { error?: string }
+1 -1
src/pages/shell.ts
··· 1 - import { type Hole, html } from '../view' 1 + import { type Hole, html } from '../lib/view' 2 2 3 3 export function shell({ title, content }: { title: string; content: Hole }) { 4 4 return html`<html>
+32 -1
src/routes.ts
··· 6 6 import type { AppContext } from '#/index' 7 7 import { home } from '#/pages/home' 8 8 import { login } from '#/pages/login' 9 - import { page } from '#/view' 9 + import { page } from '#/lib/view' 10 10 import * as Status from '#/lexicon/types/com/example/status' 11 11 12 + // Helper function for defining routes 12 13 const handler = 13 14 (fn: express.Handler) => 14 15 async ( ··· 26 27 export const createRouter = (ctx: AppContext) => { 27 28 const router = express.Router() 28 29 30 + // Static assets 29 31 router.use('/public', express.static(path.join(__dirname, 'pages', 'public'))) 30 32 33 + // OAuth metadata 31 34 router.get( 32 35 '/client-metadata.json', 33 36 handler((_req, res) => { ··· 35 38 }) 36 39 ) 37 40 41 + // OAuth callback to complete session creation 38 42 router.get( 39 43 '/oauth/callback', 40 44 handler(async (req, res) => { ··· 50 54 }) 51 55 ) 52 56 57 + // Login page 53 58 router.get( 54 59 '/login', 55 60 handler(async (_req, res) => { ··· 57 62 }) 58 63 ) 59 64 65 + // Login handler 60 66 router.post( 61 67 '/login', 62 68 handler(async (req, res) => { 69 + // Validate 63 70 const handle = req.body?.handle 64 71 if (typeof handle !== 'string' || !isValidHandle(handle)) { 65 72 return res.type('html').send(page(login({ error: 'invalid handle' }))) 66 73 } 74 + 75 + // Initiate the OAuth flow 67 76 try { 68 77 const url = await ctx.oauthClient.authorize(handle) 69 78 return res.redirect(url.toString()) ··· 83 92 }) 84 93 ) 85 94 95 + // Logout handler 86 96 router.post( 87 97 '/logout', 88 98 handler(async (req, res) => { ··· 91 101 }) 92 102 ) 93 103 104 + // Homepage 94 105 router.get( 95 106 '/', 96 107 handler(async (req, res) => { 108 + // If the user is signed in, get an agent which communicates with their server 97 109 const agent = await getSessionAgent(req, res, ctx) 110 + 111 + // Fetch data stored in our SQLite 98 112 const statuses = await ctx.db 99 113 .selectFrom('status') 100 114 .selectAll() ··· 108 122 .where('authorDid', '=', agent.accountDid) 109 123 .executeTakeFirst() 110 124 : undefined 125 + 126 + // Map user DIDs to their domain-name handles 111 127 const didHandleMap = await ctx.resolver.resolveDidsToHandles( 112 128 statuses.map((s) => s.authorDid) 113 129 ) 130 + 114 131 if (!agent) { 132 + // Serve the logged-out view 115 133 return res.type('html').send(page(home({ statuses, didHandleMap }))) 116 134 } 135 + 136 + // Fetch additional information about the logged-in user 117 137 const { data: profile } = await agent.getProfile({ 118 138 actor: agent.accountDid, 119 139 }) 140 + didHandleMap[profile.handle] = agent.accountDid 141 + 142 + // Serve the logged-in view 120 143 return res 121 144 .type('html') 122 145 .send(page(home({ statuses, didHandleMap, profile, myStatus }))) 123 146 }) 124 147 ) 125 148 149 + // "Set status" handler 126 150 router.post( 127 151 '/status', 128 152 handler(async (req, res) => { 153 + // If the user is signed in, get an agent which communicates with their server 129 154 const agent = await getSessionAgent(req, res, ctx) 130 155 if (!agent) { 131 156 return res.status(401).json({ error: 'Session required' }) 132 157 } 133 158 159 + // Construct & validate their status record 134 160 const record = { 135 161 $type: 'com.example.status', 136 162 status: req.body?.status, ··· 141 167 } 142 168 143 169 try { 170 + // Write the status record to the user's repository 144 171 await agent.com.atproto.repo.putRecord({ 145 172 repo: agent.accountDid, 146 173 collection: 'com.example.status', ··· 154 181 } 155 182 156 183 try { 184 + // Optimistically update our SQLite 185 + // This isn't strictly necessary because the write event will be 186 + // handled in #/firehose/ingestor.ts, but it ensures that future reads 187 + // will be up-to-date after this method finishes. 157 188 await ctx.db 158 189 .insertInto('status') 159 190 .values({
src/view.ts src/lib/view.ts