diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 9 | ||||
| -rw-r--r-- | src/i18n.js | 1 | ||||
| -rw-r--r-- | src/media-upload.js | 13 |
3 files changed, 19 insertions, 4 deletions
@@ -12,7 +12,7 @@ import { makeSearcher, resultRows, searchable, punkUrl, warmSearch } from './rec import { assetUrl } from './asset-url.js'; import { npubOf, seedPubkey, neventOf, parseNostrPubkey } from './nostr.js'; import { nip98Header } from './nip98.js'; -import { uploadPublicMedia, uploadStream, MediaUploadError } from './media-upload.js'; +import { uploadPublicMedia, uploadStream, mediaUploadSigner, MediaUploadError } from './media-upload.js'; import { NOSTR_MARK } from './features/nostrlogin.js'; import { scanQr } from './scan.js'; import { dataSources, getSource, setSource, getNetwork, setNetwork, NETWORKS } from './api.js'; @@ -3895,6 +3895,7 @@ function mediaProgress(stage, fraction) { } async function publicMediaUpload(file) { try { + const signEvent = await mediaUploadSigner(wallet, featureHook); // a phone video is shrunk on the device first (see video-compress.js); // a long one becomes an HLS stream, its master playlist the address if (/^video\//.test(file?.type || '')) { @@ -3903,15 +3904,17 @@ async function publicMediaUpload(file) { const stream = await streamVideo(file, (p) => mediaProgress('shrink', p)); if (stream) { mediaProgress('upload', 0); - const url = await uploadStream(stream, (event) => wallet.nostrSign(event), { onProgress: (p) => mediaProgress('upload', p) }); + const url = await uploadStream(stream, signEvent, { onProgress: (p) => mediaProgress('upload', p) }); return { url, m: 'application/vnd.apple.mpegurl', dim: stream.dim, duration: Math.round(stream.seconds) }; } mediaProgress('shrink', 0); file = await compressVideo(file, (p) => mediaProgress('shrink', p)); } mediaProgress('upload', 0); - return await uploadPublicMedia(file, (event) => wallet.nostrSign(event), { onProgress: (p) => mediaProgress('upload', p) }); + return await uploadPublicMedia(file, signEvent, { onProgress: (p) => mediaProgress('upload', p) }); } catch (error) { + if (error instanceof MediaUploadError && error.code === 'no-identity') throw new Error(t('msgNoIdentity')); + if (error instanceof MediaUploadError && error.code === 'unauthorized') throw new Error(t('mediaUploadUnauthorized')); if (error instanceof MediaUploadError && error.code === 'too-big') throw new Error(t('mediaUploadTooBig')); if (error instanceof MediaUploadError && error.code === 'unsupported') throw new Error(t('mediaUploadUnsupported')); throw new Error(t('msgUploadFailed')); diff --git a/src/i18n.js b/src/i18n.js index 562043d..998d233 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -252,6 +252,7 @@ const STR = { msgUploadFailed: 'Upload failed. Check your connection and try again', mediaUploadTooBig: 'That file is too big for the available media hosts', mediaUploadUnsupported: 'That file format is not supported', + mediaUploadUnauthorized: 'The media server denied this upload. Check that your Nostr identity is allowed to upload there.', msgPhoto: 'Photo', searchPeopleFor: 'Search people for {q}', msgEmojiSearch: 'Search emoji…', diff --git a/src/media-upload.js b/src/media-upload.js index d7a99e1..f851943 100644 --- a/src/media-upload.js +++ b/src/media-upload.js @@ -20,6 +20,16 @@ export class MediaUploadError extends Error { } } +// Private Blossom hosts authorize the user's Nostr identity, which can differ +// from the wallet's sync key. Resolve once for the whole upload (including HLS). +export async function mediaUploadSigner(wallet, hook) { + const id = hook('nostrLoginIdentity'); + if (!id) return (event) => wallet.nostrSign(event); + const signer = id.signer || await hook('nostrLoginResume'); + if (!signer) throw new MediaUploadError('no-identity'); + return (event) => signer.signEvent(event); +} + async function fileHash(file) { const hash = sha256.create(); const chunk = 4 * 1024 * 1024; @@ -64,7 +74,7 @@ function putWithProgress(url, { body, headers, signal }, onProgress) { } // BUD-02 upload. `signEvent` is deliberately injected: the app can use its -// always-available wallet identity, while tests need no secret key. +// signed-in Nostr identity, while tests need no secret key. export async function uploadPublicMedia(file, signEvent, { servers = mediaServers(PUBLIC_MEDIA_SERVERS), fetcher = globalThis.fetch, @@ -112,6 +122,7 @@ export async function uploadPublicMedia(file, signEvent, { } } const details = failures.map((x) => [x.status, x.reason].filter(Boolean).join(' ')).filter(Boolean).join('; '); + if (failures.length && failures.every((x) => x.status === 401 || x.status === 403)) throw new MediaUploadError('unauthorized', details); if (failures.length && failures.every((x) => x.status === 413)) throw new MediaUploadError('too-big', details); if (failures.length && failures.every((x) => x.status === 415)) throw new MediaUploadError('unsupported', details); throw new MediaUploadError('failed', details); |
