Scratch space for learning atproto app development
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Align code with guide

+39 -46
+6 -6
src/ingester.ts
··· 11 11 export function createIngester(db: Database) { 12 12 const logger = pino({ name: 'firehose', level: env.LOG_LEVEL }) 13 13 return new Firehose({ 14 - service: env.FIREHOSE_URL, 15 - idResolver: new IdResolver({ 16 - plcUrl: env.PLC_URL, 17 - didCache: new MemoryCache(HOUR, DAY), 18 - }), 14 + filterCollections: ['xyz.statusphere.status'], 19 15 handleEvent: async (evt: Event) => { 20 16 // Watch for write events 21 17 if (evt.event === 'create' || evt.event === 'update') { ··· 70 66 onError: (err: unknown) => { 71 67 logger.error({ err }, 'error on firehose ingestion') 72 68 }, 73 - filterCollections: ['xyz.statusphere.status'], 74 69 excludeIdentity: true, 75 70 excludeAccount: true, 71 + service: env.FIREHOSE_URL, 72 + idResolver: new IdResolver({ 73 + plcUrl: env.PLC_URL, 74 + didCache: new MemoryCache(HOUR, DAY), 75 + }), 76 76 }) 77 77 }
+5 -4
src/lib/http.ts
··· 1 + import { Request, Response } from 'express' 1 2 import { createHttpTerminator } from 'http-terminator' 2 3 import { once } from 'node:events' 3 4 import type { ··· 11 12 12 13 export type Middleware< 13 14 Req extends IncomingMessage = IncomingMessage, 14 - Res extends ServerResponse<Req> = ServerResponse<Req>, 15 + Res extends ServerResponse = ServerResponse, 15 16 > = (req: Req, res: Res, next: NextFunction) => void 16 17 17 18 export type Handler< 18 19 Req extends IncomingMessage = IncomingMessage, 19 - Res extends ServerResponse<Req> = ServerResponse<Req>, 20 + Res extends ServerResponse = ServerResponse, 20 21 > = (req: Req, res: Res) => unknown | Promise<unknown> 21 22 /** 22 23 * Wraps a request handler middleware to ensure that `next` is called if it 23 24 * throws or returns a promise that rejects. 24 25 */ 25 26 export function handler< 26 - Req extends IncomingMessage = IncomingMessage, 27 - Res extends ServerResponse<Req> = ServerResponse<Req>, 27 + Req extends IncomingMessage = Request, 28 + Res extends ServerResponse = Response, 28 29 >(fn: Handler<Req, Res>): Middleware<Req, Res> { 29 30 return async (req, res, next) => { 30 31 try {
+28 -36
src/routes.ts
··· 31 31 res: ServerResponse, 32 32 ctx: AppContext, 33 33 ) { 34 + res.setHeader('Vary', 'Cookie') 35 + 34 36 const session = await getIronSession<Session>(req, res, { 35 37 cookieName: 'sid', 36 38 password: env.COOKIE_SECRET, 37 39 }) 38 40 if (!session.did) return null 41 + 42 + // This page is dynamic and should not be cached publicly 43 + res.setHeader('cache-control', `max-age=${MAX_AGE}, private`) 44 + 39 45 try { 40 46 const oauthSession = await ctx.oauthClient.restore(session.did) 41 47 return oauthSession ? new Agent(oauthSession) : null ··· 60 66 // OAuth metadata 61 67 router.get( 62 68 '/oauth-client-metadata.json', 63 - handler((req: Request, res: Response) => { 69 + handler((req, res) => { 64 70 res.setHeader('cache-control', `max-age=${MAX_AGE}, public`) 65 71 res.json(ctx.oauthClient.clientMetadata) 66 72 }), ··· 69 75 // Public keys 70 76 router.get( 71 77 '/.well-known/jwks.json', 72 - handler((req: Request, res: Response) => { 78 + handler((req, res) => { 73 79 res.setHeader('cache-control', `max-age=${MAX_AGE}, public`) 74 80 res.json(ctx.oauthClient.jwks) 75 81 }), ··· 78 84 // OAuth callback to complete session creation 79 85 router.get( 80 86 '/oauth/callback', 81 - handler(async (req: Request, res: Response) => { 87 + handler(async (req, res) => { 82 88 res.setHeader('cache-control', 'no-store') 83 89 84 90 const params = new URLSearchParams(req.originalUrl.split('?')[1]) ··· 116 122 // Login page 117 123 router.get( 118 124 '/login', 119 - handler(async (req: Request, res: Response) => { 125 + handler(async (req, res) => { 120 126 res.setHeader('cache-control', `max-age=${MAX_AGE}, public`) 121 - 122 - return res.type('html').send(page(login({}))) 127 + res.type('html').send(page(login({}))) 123 128 }), 124 129 ) 125 130 ··· 127 132 router.post( 128 133 '/login', 129 134 express.urlencoded(), 130 - handler(async (req: Request, res: Response) => { 135 + handler(async (req, res) => { 136 + // Never store this route 131 137 res.setHeader('cache-control', 'no-store') 132 138 133 - const input = ifString(req.body.input) 134 - 135 - // Validate 136 - if (!input) { 137 - return res.type('html').send(page(login({ error: 'invalid input' }))) 138 - } 139 - 140 - // @NOTE "input" can be a handle, a DID or a service URL (PDS). 141 - 142 139 // Initiate the OAuth flow 143 140 try { 141 + // Validate input: can be a handle, a DID or a service URL (PDS). 142 + const input = ifString(req.body.input) 143 + if (!input) { 144 + throw new Error('Invalid input') 145 + } 146 + 147 + // Initiate the OAuth flow 144 148 const url = await ctx.oauthClient.authorize(input, { 145 149 scope: 'atproto transition:generic', 146 150 }) 151 + 147 152 res.redirect(url.toString()) 148 153 } catch (err) { 149 154 ctx.logger.error({ err }, 'oauth authorize failed') 150 155 151 - const error = 152 - err instanceof OAuthResolverError 153 - ? err.message 154 - : "couldn't initiate login" 156 + const error = err instanceof Error ? err.message : 'unexpected error' 155 157 156 158 return res.type('html').send(page(login({ error }))) 157 159 } ··· 161 163 // Signup 162 164 router.get( 163 165 '/signup', 164 - handler(async (req: Request, res: Response) => { 166 + handler(async (req, res) => { 165 167 res.setHeader('cache-control', `max-age=${MAX_AGE}, public`) 166 168 167 169 try { ··· 189 191 // Logout handler 190 192 router.post( 191 193 '/logout', 192 - handler(async (req: Request, res: Response) => { 194 + handler(async (req, res) => { 193 195 // Never store this route 194 196 res.setHeader('cache-control', 'no-store') 195 197 ··· 217 219 // Homepage 218 220 router.get( 219 221 '/', 220 - handler(async (req: Request, res: Response) => { 221 - // Prevent caching of this page when the credentials change 222 - res.setHeader('Vary', 'Cookie') 223 - 222 + handler(async (req, res) => { 224 223 // If the user is signed in, get an agent which communicates with their server 225 224 const agent = await getSessionAgent(req, res, ctx) 226 225 ··· 250 249 return res.type('html').send(page(home({ statuses, didHandleMap }))) 251 250 } 252 251 253 - // Make sure this page does not get cached in public caches (proxies) 254 - res.setHeader('cache-control', 'private') 255 - 256 252 // Fetch additional information about the logged-in user 257 253 const profileResponse = await agent.com.atproto.repo 258 254 .getRecord({ ··· 282 278 router.post( 283 279 '/status', 284 280 express.urlencoded(), 285 - handler(async (req: Request, res: Response) => { 286 - // Never store this route 287 - res.setHeader('cache-control', 'no-store') 288 - 281 + handler(async (req, res) => { 289 282 // If the user is signed in, get an agent which communicates with their server 290 283 const agent = await getSessionAgent(req, res, ctx) 291 284 if (!agent) { ··· 295 288 .send('<h1>Error: Session required</h1>') 296 289 } 297 290 298 - // Construct & validate their status record 299 - const rkey = TID.nextStr() 291 + // Construct their status record 300 292 const record = { 301 293 $type: 'xyz.statusphere.status', 302 294 status: req.body?.status, ··· 317 309 const res = await agent.com.atproto.repo.putRecord({ 318 310 repo: agent.assertDid, 319 311 collection: 'xyz.statusphere.status', 320 - rkey, 312 + rkey: TID.nextStr(), 321 313 record, 322 314 validate: false, 323 315 })