mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

[Video] Check upload limits before uploading (#5153)

* DRY up video service auth code

* throw error if over upload limits

* use token

* xmark on toast

* errors with nice translatable error messages

* Update src/state/queries/video/video.ts

---------

Co-authored-by: Hailey <me@haileyok.com>

authored by samuel.fm

Hailey and committed by
GitHub
45a719b2 b7d78fe5

+146 -46
+3
src/lib/constants.ts
··· 137 137 138 138 export const MAX_LABELERS = 20 139 139 140 + export const VIDEO_SERVICE = 'https://video.bsky.app' 141 + export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app' 142 + 140 143 export const SUPPORTED_MIME_TYPES = [ 141 144 'video/mp4', 142 145 'video/mpeg',
+7
src/lib/media/video/errors.ts
··· 11 11 this.name = 'ServerError' 12 12 } 13 13 } 14 + 15 + export class UploadLimitError extends Error { 16 + constructor(message: string) { 17 + super(message) 18 + this.name = 'UploadLimitError' 19 + } 20 + }
+3 -5
src/state/queries/video/util.ts
··· 1 1 import {useMemo} from 'react' 2 2 import {AtpAgent} from '@atproto/api' 3 3 4 - import {SupportedMimeTypes} from '#/lib/constants' 5 - 6 - const UPLOAD_ENDPOINT = 'https://video.bsky.app/' 4 + import {SupportedMimeTypes, VIDEO_SERVICE} from '#/lib/constants' 7 5 8 6 export const createVideoEndpointUrl = ( 9 7 route: string, 10 8 params?: Record<string, string>, 11 9 ) => { 12 - const url = new URL(`${UPLOAD_ENDPOINT}`) 10 + const url = new URL(VIDEO_SERVICE) 13 11 url.pathname = route 14 12 if (params) { 15 13 for (const key in params) { ··· 22 20 export function useVideoAgent() { 23 21 return useMemo(() => { 24 22 return new AtpAgent({ 25 - service: UPLOAD_ENDPOINT, 23 + service: VIDEO_SERVICE, 26 24 }) 27 25 }, []) 28 26 }
+73
src/state/queries/video/video-upload.shared.ts
··· 1 + import {useCallback} from 'react' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 4 + 5 + import {VIDEO_SERVICE_DID} from '#/lib/constants' 6 + import {UploadLimitError} from '#/lib/media/video/errors' 7 + import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers' 8 + import {useAgent} from '#/state/session' 9 + import {useVideoAgent} from './util' 10 + 11 + export function useServiceAuthToken({ 12 + aud, 13 + lxm, 14 + exp, 15 + }: { 16 + aud?: string 17 + lxm: string 18 + exp?: number 19 + }) { 20 + const agent = useAgent() 21 + 22 + return useCallback(async () => { 23 + const pdsAud = getServiceAuthAudFromUrl(agent.dispatchUrl) 24 + 25 + if (!pdsAud) { 26 + throw new Error('Agent does not have a PDS URL') 27 + } 28 + 29 + const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth({ 30 + aud: aud ?? pdsAud, 31 + lxm, 32 + exp, 33 + }) 34 + 35 + return serviceAuth.token 36 + }, [agent, aud, lxm, exp]) 37 + } 38 + 39 + export function useVideoUploadLimits() { 40 + const agent = useVideoAgent() 41 + const getToken = useServiceAuthToken({ 42 + lxm: 'app.bsky.video.getUploadLimits', 43 + aud: VIDEO_SERVICE_DID, 44 + }) 45 + const {_} = useLingui() 46 + 47 + return useCallback(async () => { 48 + const {data: limits} = await agent.app.bsky.video 49 + .getUploadLimits( 50 + {}, 51 + {headers: {Authorization: `Bearer ${await getToken()}`}}, 52 + ) 53 + .catch(err => { 54 + if (err instanceof Error) { 55 + throw new UploadLimitError(err.message) 56 + } else { 57 + throw err 58 + } 59 + }) 60 + 61 + if (!limits.canUpload) { 62 + if (limits.message) { 63 + throw new UploadLimitError(limits.message) 64 + } else { 65 + throw new UploadLimitError( 66 + _( 67 + msg`You have temporarily reached the limit for video uploads. Please try again later.`, 68 + ), 69 + ) 70 + } 71 + } 72 + }, [agent, _, getToken]) 73 + }
+10 -18
src/state/queries/video/video-upload.ts
··· 9 9 import {ServerError} from '#/lib/media/video/errors' 10 10 import {CompressedVideo} from '#/lib/media/video/types' 11 11 import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util' 12 - import {useAgent, useSession} from '#/state/session' 13 - import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers' 12 + import {useSession} from '#/state/session' 13 + import {useServiceAuthToken, useVideoUploadLimits} from './video-upload.shared' 14 14 15 15 export const useUploadVideoMutation = ({ 16 16 onSuccess, ··· 24 24 signal: AbortSignal 25 25 }) => { 26 26 const {currentAccount} = useSession() 27 - const agent = useAgent() 27 + const getToken = useServiceAuthToken({ 28 + lxm: 'com.atproto.repo.uploadBlob', 29 + exp: Date.now() / 1000 + 60 * 30, // 30 minutes 30 + }) 31 + const checkLimits = useVideoUploadLimits() 28 32 const {_} = useLingui() 29 33 30 34 return useMutation({ 31 35 mutationKey: ['video', 'upload'], 32 36 mutationFn: cancelable(async (video: CompressedVideo) => { 37 + await checkLimits() 38 + 33 39 const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { 34 40 did: currentAccount!.did, 35 41 name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`, 36 42 }) 37 43 38 - const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl) 39 - 40 - if (!serviceAuthAud) { 41 - throw new Error('Agent does not have a PDS URL') 42 - } 43 - 44 - const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth( 45 - { 46 - aud: serviceAuthAud, 47 - lxm: 'com.atproto.repo.uploadBlob', 48 - exp: Date.now() / 1000 + 60 * 30, // 30 minutes 49 - }, 50 - ) 51 - 52 44 const uploadTask = createUploadTask( 53 45 uri, 54 46 video.uri, 55 47 { 56 48 headers: { 57 49 'content-type': video.mimeType, 58 - Authorization: `Bearer ${serviceAuth.token}`, 50 + Authorization: `Bearer ${await getToken()}`, 59 51 }, 60 52 httpMethod: 'POST', 61 53 uploadType: FileSystemUploadType.BINARY_CONTENT,
+12 -19
src/state/queries/video/video-upload.web.ts
··· 8 8 import {ServerError} from '#/lib/media/video/errors' 9 9 import {CompressedVideo} from '#/lib/media/video/types' 10 10 import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util' 11 - import {useAgent, useSession} from '#/state/session' 12 - import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers' 11 + import {useSession} from '#/state/session' 12 + import {useServiceAuthToken, useVideoUploadLimits} from './video-upload.shared' 13 13 14 14 export const useUploadVideoMutation = ({ 15 15 onSuccess, ··· 23 23 signal: AbortSignal 24 24 }) => { 25 25 const {currentAccount} = useSession() 26 - const agent = useAgent() 26 + const getToken = useServiceAuthToken({ 27 + lxm: 'com.atproto.repo.uploadBlob', 28 + exp: Date.now() / 1000 + 60 * 30, // 30 minutes 29 + }) 30 + const checkLimits = useVideoUploadLimits() 27 31 const {_} = useLingui() 28 32 29 33 return useMutation({ 30 34 mutationKey: ['video', 'upload'], 31 35 mutationFn: cancelable(async (video: CompressedVideo) => { 36 + await checkLimits() 37 + 32 38 const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { 33 39 did: currentAccount!.did, 34 40 name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`, 35 41 }) 36 42 37 - const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl) 38 - 39 - if (!serviceAuthAud) { 40 - throw new Error('Agent does not have a PDS URL') 41 - } 42 - 43 - const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth( 44 - { 45 - aud: serviceAuthAud, 46 - lxm: 'com.atproto.repo.uploadBlob', 47 - exp: Date.now() / 1000 + 60 * 30, // 30 minutes 48 - }, 49 - ) 50 - 51 43 let bytes = video.bytes 52 - 53 44 if (!bytes) { 54 45 bytes = await fetch(video.uri).then(res => res.arrayBuffer()) 55 46 } 47 + 48 + const token = await getToken() 56 49 57 50 const xhr = new XMLHttpRequest() 58 51 const res = await new Promise<AppBskyVideoDefs.JobStatus>( ··· 76 69 } 77 70 xhr.open('POST', uri) 78 71 xhr.setRequestHeader('Content-Type', video.mimeType) 79 - xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`) 72 + xhr.setRequestHeader('Authorization', `Bearer ${token}`) 80 73 xhr.send(bytes) 81 74 }, 82 75 )
+37 -3
src/state/queries/video/video.ts
··· 9 9 import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants' 10 10 import {logger} from '#/logger' 11 11 import {isWeb} from '#/platform/detection' 12 - import {ServerError, VideoTooLargeError} from 'lib/media/video/errors' 12 + import { 13 + ServerError, 14 + UploadLimitError, 15 + VideoTooLargeError, 16 + } from 'lib/media/video/errors' 13 17 import {CompressedVideo} from 'lib/media/video/types' 14 18 import {useCompressVideoMutation} from 'state/queries/video/compress-video' 15 19 import {useVideoAgent} from 'state/queries/video/util' ··· 149 153 onError: e => { 150 154 if (e instanceof AbortError) { 151 155 return 152 - } else if (e instanceof ServerError) { 156 + } else if (e instanceof ServerError || e instanceof UploadLimitError) { 157 + let message 158 + // https://github.com/bluesky-social/tango/blob/lumi/lumi/worker/permissions.go#L77 159 + switch (e.message) { 160 + case 'User is not allowed to upload videos': 161 + message = _(msg`You are not allowed to upload videos.`) 162 + break 163 + case 'Uploading is disabled at the moment': 164 + message = _( 165 + msg`Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!`, 166 + ) 167 + break 168 + case "Failed to get user's upload stats": 169 + message = _( 170 + msg`We were unable to determine if you are allowed to upload videos. Please try again.`, 171 + ) 172 + break 173 + case 'User has exceeded daily upload bytes limit': 174 + message = _( 175 + msg`You've reached your daily limit for video uploads (too many bytes)`, 176 + ) 177 + break 178 + case 'User has exceeded daily upload videos limit': 179 + message = _( 180 + msg`You've reached your daily limit for video uploads (too many videos)`, 181 + ) 182 + break 183 + default: 184 + message = e.message 185 + break 186 + } 153 187 dispatch({ 154 188 type: 'SetError', 155 - error: e.message, 189 + error: message, 156 190 }) 157 191 } else { 158 192 dispatch({
+1 -1
src/view/com/composer/videos/VideoPreview.web.tsx
··· 42 42 ref.current.addEventListener( 43 43 'error', 44 44 () => { 45 - Toast.show(_(msg`Could not process your video`)) 45 + Toast.show(_(msg`Could not process your video`), 'xmark') 46 46 clear() 47 47 }, 48 48 {signal},