Bluesky app fork with some witchin' additions 馃挮
at post-text-option 114 lines 3.4 kB view raw
1import assert from 'node:assert' 2 3import bodyParser from 'body-parser' 4import {type Express, type Request} from 'express' 5 6import {type AppContext} from '../context.js' 7import {LinkType} from '../db/schema.js' 8import {randomId} from '../util.js' 9import {handler} from './util.js' 10 11export default function (ctx: AppContext, app: Express) { 12 return app.post( 13 '/link', 14 bodyParser.json(), 15 handler(async (req, res) => { 16 let path: string 17 if (typeof req.body?.path === 'string') { 18 path = req.body.path 19 } else { 20 return res.status(400).json({ 21 error: 'InvalidPath', 22 message: '"path" parameter is missing or not a string', 23 }) 24 } 25 if (!path.startsWith('/')) { 26 return res.status(400).json({ 27 error: 'InvalidPath', 28 message: 29 '"path" parameter must be formatted as a path, starting with a "/"', 30 }) 31 } 32 const parts = getPathParts(path) 33 if (parts.length === 3 && parts[0] === 'start') { 34 // link pattern: /start/{did}/{rkey} 35 if (!parts[1].startsWith('did:')) { 36 // enforce strong links 37 return res.status(400).json({ 38 error: 'InvalidPath', 39 message: 40 '"path" parameter for starter pack must contain the actor\'s DID', 41 }) 42 } 43 const id = await ensureLink(ctx, LinkType.StarterPack, parts) 44 return res.json({url: getUrl(ctx, req, id)}) 45 } 46 return res.status(400).json({ 47 error: 'InvalidPath', 48 message: '"path" parameter does not have a known format', 49 }) 50 }), 51 ) 52} 53 54const ensureLink = async (ctx: AppContext, type: LinkType, parts: string[]) => { 55 const normalizedPath = normalizedPathFromParts(parts) 56 const created = await ctx.db.db 57 .insertInto('link') 58 .values({ 59 id: randomId(), 60 type, 61 path: normalizedPath, 62 }) 63 .onConflict(oc => oc.column('path').doNothing()) 64 .returningAll() 65 .executeTakeFirst() 66 if (created) { 67 return created.id 68 } 69 const found = await ctx.db.db 70 .selectFrom('link') 71 .selectAll() 72 .where('path', '=', normalizedPath) 73 .executeTakeFirstOrThrow() 74 return found.id 75} 76 77const getUrl = (ctx: AppContext, req: Request, id: string) => { 78 if (!ctx.cfg.service.hostnames.length) { 79 assert(req.headers.host, 'request must be made with host header') 80 const baseUrl = 81 req.protocol === 'http' && req.headers.host.startsWith('localhost:') 82 ? `http://${req.headers.host}` 83 : `https://${req.headers.host}` 84 return `${baseUrl}/${id}` 85 } 86 const host = req.headers.host ?? '' 87 const baseUrl = ctx.cfg.service.hostnamesSet.has(host) 88 ? `https://${host}` 89 : `https://${ctx.cfg.service.hostnames[0]}` 90 return `${baseUrl}/${id}` 91} 92 93const normalizedPathFromParts = (parts: string[]): string => { 94 // When given ['path1', 'path2', 'te:fg'], output should be 95 // /path1/path2/te:fg 96 return ( 97 '/' + 98 parts 99 .map(encodeURIComponent) 100 .map(part => part.replace(/%3A/g, ':')) // preserve colons 101 .join('/') 102 ) 103} 104 105const getPathParts = (path: string): string[] => { 106 if (path === '/') return [] 107 if (path.endsWith('/')) { 108 path = path.slice(0, -1) // ignore trailing slash 109 } 110 return path 111 .slice(1) // remove leading slash 112 .split('/') 113 .map(decodeURIComponent) 114}