diff --git a/examples/apps/README.md b/examples/apps/README.md index c8b0730..f17559f 100644 --- a/examples/apps/README.md +++ b/examples/apps/README.md @@ -1,13 +1,14 @@ # Example apps -Two working apps for the site platform. Both discover whose repo they serve -from their own origin, so either one runs on any account that installs it. -[docs/apps.md](../../docs/apps.md) explains how they are put together. +Three working apps for the site platform. Each one discovers whose repo it +serves from its own origin, so any of them runs on any account that installs +it. [docs/apps.md](../../docs/apps.md) explains how they are put together. | App | What it does | |-----|--------------| | `photos.mjs` | Galleries from `social.grain.*` records: justified layouts, a lightbox, terrain headers for located galleries, a map, favourite counts from the network | | `roasts.mjs` | A coffee roast log: a live roast timer, weight-loss and development-time metrics against Sweet Maria's roast card, notes and photos | +| `drop.mjs` | An image host: drag, paste or choose a file and the link copies itself. The link is the site's `/.blobs/` route, so it works anywhere an image URL does | Each script deploys the site to your repo and publishes the manifest that makes it installable: @@ -23,5 +24,7 @@ Then install it from the account page's Sites section, or with `pdsjs-site install at://you.example.com/dev.pdsjs.app.manifest/photos`. `roasts.mjs` writes to `com.chadtmiller.coffee.roast` unless -`ROAST_COLLECTION` names another. An NSID's authority is every segment but the -last, reversed, so use a domain you control. +`ROAST_COLLECTION` names another, and `drop.mjs` writes to +`com.chadtmiller.drop.image` unless `DROP_COLLECTION` names another. An NSID's +authority is every segment but the last, reversed, so use a domain you +control. diff --git a/examples/apps/drop.mjs b/examples/apps/drop.mjs new file mode 100644 index 0000000..a771b4a --- /dev/null +++ b/examples/apps/drop.mjs @@ -0,0 +1,769 @@ +// An image host: drop a file in, get a link to share. Deploy it to your own +// PDS with PDS_URL, PDS_DID and PDS_APP_PASSWORD set; see docs/apps.md. +// +// Images are com.chadtmiller.drop.image records in the owner's repo and the +// bytes are ordinary blobs. The shared link is the site's /.blobs/ +// route, which serves the original bytes immutably. Reads are public through +// dev.pdsjs.query.run; uploads need an app password, which the app exchanges +// for a session against the owner's own PDS. +// +// Usage: PDS_APP_PASSWORD=... node drop.mjs + +const PDS = process.env.PDS_URL; +const DID = process.env.PDS_DID; +if (!PDS || !DID) throw new Error('Set PDS_URL and PDS_DID.'); +const SITE = 'drop-app'; +const SITES = 'dev.pdsjs.site.deploy'; +const DEFS = 'dev.pdsjs.query.def'; +// The collection images are written to. An NSID's authority is every segment +// but the last, reversed, so this names a domain you control. +const IMAGES = process.env.DROP_COLLECTION || 'com.chadtmiller.drop.image'; + +async function xrpc(path, init = {}) { + const response = await fetch(`${PDS}/xrpc/${path}`, init); + if (!response.ok) { + throw new Error( + `${path.split('?')[0]} ${response.status}: ${(await response.text()).slice(0, 200)}`, + ); + } + return response.json(); +} + +const password = process.env.PDS_APP_PASSWORD; +if (!password) throw new Error('Set PDS_APP_PASSWORD.'); +const session = await xrpc('com.atproto.server.createSession', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier: DID, password }), +}); +const auth = { Authorization: `Bearer ${session.accessJwt}` }; + +/** @param {string} collection @param {string} rkey @param {Record} record */ +async function put(collection, rkey, record) { + await xrpc('com.atproto.repo.putRecord', { + method: 'POST', + headers: { ...auth, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + repo: DID, + collection, + rkey, + record: { $type: collection, ...record }, + }), + }); + console.error(`put ${collection}/${rkey}`); +} + +// ---- the query the app reads ---- + +await put(DEFS, 'drop-images', { + collection: IMAGES, + sort: { field: 'createdAt', direction: 'desc' }, + createdAt: new Date().toISOString(), +}); + +// ---- the app ---- + +const HTML = ` + + + + +Drop + + + + + +

Loading…

+
+ + + + +`; + +// ---- deploy ---- + +const upload = await fetch(`${PDS}/xrpc/com.atproto.repo.uploadBlob`, { + method: 'POST', + headers: { ...auth, 'Content-Type': 'text/html; charset=utf-8' }, + body: HTML, +}); +const blob = (await upload.json()).blob; +await put(SITES, SITE, { + files: [ + { + path: 'index.html', + blob, + contentType: 'text/html; charset=utf-8', + cacheControl: 'public, max-age=300', + }, + ], + fallback: 'index.html', + createdAt: new Date().toISOString(), +}); +console.error( + `Deployed ${SITE}: ${Math.round(HTML.length / 1024)}KB, one file.`, +); + +// ---- publish it as an installable app ---- + +const ICON = ` + + + +`; +const iconUp = await fetch(`${PDS}/xrpc/com.atproto.repo.uploadBlob`, { + method: 'POST', + headers: { ...auth, 'Content-Type': 'image/svg+xml' }, + body: ICON, +}); +const icon = (await iconUp.json()).blob; + +const deployed = await xrpc( + `com.atproto.repo.getRecord?repo=${DID}&collection=${SITES}&rkey=${SITE}`, +); +await put('dev.pdsjs.app.manifest', 'drop', { + name: 'Drop', + description: + 'An image host in your own repo: drag, paste or choose a file and the link copies itself. Images are blobs served immutably from your site, so the link works anywhere an image URL does.', + icon, + site: { uri: deployed.uri, cid: deployed.cid }, + suggestedSite: 'drop', + primitives: ['serve', 'query'], + external: false, + queries: [ + { + name: 'drop-images', + collection: IMAGES, + sort: { field: 'createdAt', direction: 'desc' }, + }, + ], + settings: [ + { + key: 'gallery', + label: 'Show every image to visitors', + type: 'boolean', + default: true, + }, + ], + createdAt: new Date().toISOString(), +}); +console.error('Published dev.pdsjs.app.manifest/drop.'); diff --git a/examples/apps/package.json b/examples/apps/package.json index 4213666..13cde86 100644 --- a/examples/apps/package.json +++ b/examples/apps/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "photos": "node photos.mjs", - "roasts": "node roasts.mjs" + "roasts": "node roasts.mjs", + "drop": "node drop.mjs" } }