Changeset ef519a3 in Klonkt for src/routes/activitypub.js
- Timestamp:
- 07/30/2026 11:32:06 PM (6 weeks ago)
- Branches:
- main
- Children:
- 2a17f0a
- Parents:
- 1a2f206
- File:
-
- 1 edited
-
src/routes/activitypub.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r1a2f206 ref519a3 124 124 125 125 // ── Follow-QR (Robins verzoek, 31-7) ────────────────────────────── 126 // A PNG QR of share:social/follow/AP/@slug@host: the ward shows it in 127 // Account, a friend scans the SCREEN with the ordinary camera app and their 128 // Shaer opens with the follow question. Public on purpose: it encodes only 129 // the public handle, and the app's plain image loaders carry no bearer. 126 // The QR carries an HTTPS url, not the share: scheme: camera apps (Google 127 // Lens voorop) treat unknown schemes as plain text and only offer to OPEN 128 // https links (Robins melding, 31-7). The url lands on the interstitial 129 // below, whose one big button fires the share: scheme — from a browser the 130 // custom scheme DOES work (BROWSABLE intent-filter; Safari prompts). 131 // Public on purpose: it encodes only the public handle, and the app's plain 132 // image loaders carry no bearer. 130 133 router.get('/ap/users/:slug/follow-qr.png', async (req, res) => { 131 134 const site = db.prepare('SELECT slug FROM sites WHERE slug = ?').get(req.params.slug); 132 135 if (!site) return res.status(404).end(); 133 136 try { 134 const host = new URL(baseUrl(req)).host;135 137 const { default: QRCode } = await import('qrcode'); 136 const png = await QRCode.toBuffer(` share:social/follow/AP/@${site.slug}@${host}`, { width: 600, margin: 1 });138 const png = await QRCode.toBuffer(`${baseUrl(req)}/ap/users/${encodeURIComponent(site.slug)}/follow`, { width: 600, margin: 1 }); 137 139 res.set('Content-Type', 'image/png'); 138 140 res.set('Cache-Control', 'public, max-age=86400'); … … 142 144 res.status(500).end(); 143 145 } 146 }); 147 148 // The interstitial the QR opens: one big button into Shaer, and the handle 149 // in plain sight for whoever has no Shaer (yet). 150 router.get('/ap/users/:slug/follow', (req, res) => { 151 const site = db.prepare('SELECT slug, title FROM sites WHERE slug = ?').get(req.params.slug); 152 if (!site) return res.status(404).end(); 153 const host = new URL(baseUrl(req)).host; 154 const esc = (t) => String(t).replace(/[<>&"]/g, (c) => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c])); 155 const handle = `@${site.slug}@${host}`; 156 const name = esc(site.title || site.slug); 157 res.set('Cache-Control', 'public, max-age=3600'); 158 res.send(`<!doctype html><html lang="en"><head><meta charset="utf-8"> 159 <meta name="viewport" content="width=device-width, initial-scale=1"> 160 <title>Follow ${name}</title> 161 <style> 162 body { font-family: system-ui, sans-serif; margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; 163 background: linear-gradient(160deg, #5A32E6, #2a1a5e); color: #fff; text-align: center; } 164 main { padding: 32px; max-width: 420px; } 165 h1 { font-size: 1.5rem; margin: 0 0 .4rem; } 166 .handle { opacity: .85; font-family: ui-monospace, monospace; word-break: break-all; } 167 a.go { display: block; margin: 28px auto 14px; padding: 16px 28px; border-radius: 999px; background: #fff; color: #2a1a5e; 168 font-weight: 700; font-size: 1.15rem; text-decoration: none; } 169 p.small { font-size: .85rem; opacity: .75; line-height: 1.5; } 170 </style></head><body><main> 171 <h1>Follow ${name}</h1> 172 <div class="handle">${esc(handle)}</div> 173 <a class="go" href="share:social/follow/AP/${esc(handle)}">Open in Shaer</a> 174 <p class="small">No Shaer? Any fediverse app can follow ${esc(handle)}.</p> 175 </main></body></html>`); 144 176 }); 145 177
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)