From ee132179aad680dbce7c7461564131e11fb8d490 Mon Sep 17 00:00:00 2001 From: Natsumi Date: Wed, 24 Sep 2025 23:16:48 +1200 Subject: [PATCH] Fix uploading world images --- Dotnet/AppApi/Cef/ImageUploading.cs | 33 ++ Dotnet/AppApi/Common/AppApiCommon.cs | 8 - Dotnet/VRCX-Cef.csproj | 8 + Dotnet/libs/Blake2Sharp.dll | Bin 0 -> 24064 bytes Dotnet/libs/README.md | 8 + Dotnet/libs/librsync.net.dll | Bin 0 -> 22016 bytes src/api/image.js | 297 ++++++++++++++++++ src/api/index.js | 7 +- .../AvatarDialog/ChangeAvatarImageDialog.vue | 190 ++++++++++- .../WorldDialog/ChangeWorldImageDialog.vue | 174 +++++++++- .../dialogs/WorldDialog/WorldDialog.vue | 8 +- src/types/globals.d.ts | 6 +- 12 files changed, 715 insertions(+), 24 deletions(-) create mode 100644 Dotnet/AppApi/Cef/ImageUploading.cs create mode 100644 Dotnet/libs/Blake2Sharp.dll create mode 100644 Dotnet/libs/librsync.net.dll create mode 100644 src/api/image.js diff --git a/Dotnet/AppApi/Cef/ImageUploading.cs b/Dotnet/AppApi/Cef/ImageUploading.cs new file mode 100644 index 00000000..2c435c72 --- /dev/null +++ b/Dotnet/AppApi/Cef/ImageUploading.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using librsync.net; + +namespace VRCX; + +public partial class AppApi +{ + public string MD5File(string blob) + { + var fileData = Convert.FromBase64CharArray(blob.ToCharArray(), 0, blob.Length); + using var md5 = MD5.Create(); + var md5Hash = md5.ComputeHash(fileData); + return Convert.ToBase64String(md5Hash); + } + + public string SignFile(string blob) + { + var fileData = Convert.FromBase64String(blob); + using var sig = Librsync.ComputeSignature(new MemoryStream(fileData)); + using var memoryStream = new MemoryStream(); + sig.CopyTo(memoryStream); + var sigBytes = memoryStream.ToArray(); + return Convert.ToBase64String(sigBytes); + } + + public string FileLength(string blob) + { + var fileData = Convert.FromBase64String(blob); + return fileData.Length.ToString(); + } +} \ No newline at end of file diff --git a/Dotnet/AppApi/Common/AppApiCommon.cs b/Dotnet/AppApi/Common/AppApiCommon.cs index 4c6a6e79..70c88ad7 100644 --- a/Dotnet/AppApi/Common/AppApiCommon.cs +++ b/Dotnet/AppApi/Common/AppApiCommon.cs @@ -28,14 +28,6 @@ namespace VRCX } }; - public string MD5File(string blob) - { - var fileData = Convert.FromBase64CharArray(blob.ToCharArray(), 0, blob.Length); - using var md5 = MD5.Create(); - var md5Hash = md5.ComputeHash(fileData); - return Convert.ToBase64String(md5Hash); - } - public int GetColourFromUserID(string userId) { var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId)); diff --git a/Dotnet/VRCX-Cef.csproj b/Dotnet/VRCX-Cef.csproj index 46728430..ef3e4931 100644 --- a/Dotnet/VRCX-Cef.csproj +++ b/Dotnet/VRCX-Cef.csproj @@ -61,6 +61,14 @@ + + + libs\Blake2Sharp.dll + + + libs\librsync.net.dll + + diff --git a/Dotnet/libs/Blake2Sharp.dll b/Dotnet/libs/Blake2Sharp.dll new file mode 100644 index 0000000000000000000000000000000000000000..43b09138b34890a27572bb4f0133d2cb9c3f6a94 GIT binary patch literal 24064 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 literal 0 HcmV?d00001 diff --git a/Dotnet/libs/README.md b/Dotnet/libs/README.md index 1d5cdf46..a62daf8a 100644 --- a/Dotnet/libs/README.md +++ b/Dotnet/libs/README.md @@ -1,3 +1,11 @@ +### librsync.net.dll + +- [https://github.com/braddodson/librsync.net](https://github.com/braddodson/librsync.net) + +### Blake2Sharp.dll + +- [https://github.com/BLAKE2/BLAKE2/tree/master/csharp](https://github.com/BLAKE2/BLAKE2/tree/master/csharp) + ### openvr_api.dll - [https://github.com/ValveSoftware/openvr](https://github.com/ValveSoftware/openvr) diff --git a/Dotnet/libs/librsync.net.dll b/Dotnet/libs/librsync.net.dll new file mode 100644 index 0000000000000000000000000000000000000000..0324aff5d07f4459d5499ef026efca8af34e7d2f GIT binary patch literal 22016 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 VO#fvJ>&^dO4gN console.error('Failed to finish signature:', err)); + request(`file/${fileId}/${fileVersion}/file/finish`, { + method: 'PUT' + }).catch(err => console.error('Failed to finish file:', err)); + } catch (error) { + console.error('Failed to cleanup avatar upload:', error); + } + avatarStore.avatarDialog.loading = false; + }, + + async uploadAvatarImage(params, fileId) { + try { + return await request(`file/${fileId}`, { + method: 'POST', + params + }).then((json) => { + const args = { + json, + params, + fileId + }; + return args; + }); + } catch (err) { + console.error(err); + imageReq.uploadAvatarFailCleanup(fileId); + throw err; + } + }, + + async uploadAvatarImageFileStart(params) { + try { + return await request( + `file/${params.fileId}/${params.fileVersion}/file/start`, + { + method: 'PUT' + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + } catch (err) { + console.error(err); + imageReq.uploadAvatarFailCleanup(params.fileId); + } + }, + + uploadAvatarImageFileFinish(params) { + return request( + `file/${params.fileId}/${params.fileVersion}/file/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 + } + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + }, + + async uploadAvatarImageSigStart(params) { + try { + return await request( + `file/${params.fileId}/${params.fileVersion}/signature/start`, + { + method: 'PUT' + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + } catch (err) { + console.error(err); + imageReq.uploadAvatarFailCleanup(params.fileId); + } + }, + + uploadAvatarImageSigFinish(params) { + return request( + `file/${params.fileId}/${params.fileVersion}/signature/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 + } + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + }, + + setAvatarImage(params) { + return request(`avatars/${params.id}`, { + method: 'PUT', + params + }).then((json) => { + const args = { + json, + params + }; + return args; + }); + }, + + async uploadWorldFailCleanup(id) { + const worldStore = useWorldStore(); + try { + const json = await request(`file/${id}`, { + method: 'GET' + }); + const fileId = json.id; + const fileVersion = json.versions[json.versions.length - 1].version; + request(`file/${fileId}/${fileVersion}/signature/finish`, { + method: 'PUT' + }).catch(err => console.error('Failed to finish signature:', err)); + request(`file/${fileId}/${fileVersion}/file/finish`, { + method: 'PUT' + }).catch(err => console.error('Failed to finish file:', err)); + } catch (error) { + console.error('Failed to cleanup world upload:', error); + } + worldStore.worldDialog.loading = false; + }, + + async uploadWorldImage(params, fileId) { + try { + return await request(`file/${fileId}`, { + method: 'POST', + params + }).then((json) => { + const args = { + json, + params, + fileId + }; + return args; + }); + } catch (err) { + console.error(err); + imageReq.uploadWorldFailCleanup(fileId); + } + return void 0; + }, + + async uploadWorldImageFileStart(params) { + try { + return await request( + `file/${params.fileId}/${params.fileVersion}/file/start`, + { + method: 'PUT' + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + } catch (err) { + console.error(err); + imageReq.uploadWorldFailCleanup(params.fileId); + } + return void 0; + }, + + uploadWorldImageFileFinish(params) { + return request( + `file/${params.fileId}/${params.fileVersion}/file/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 + } + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + }, + + async uploadWorldImageSigStart(params) { + try { + return await request( + `file/${params.fileId}/${params.fileVersion}/signature/start`, + { + method: 'PUT' + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + } catch (err) { + console.error(err); + imageReq.uploadWorldFailCleanup(params.fileId); + } + return void 0; + }, + + uploadWorldImageSigFinish(params) { + return request( + `file/${params.fileId}/${params.fileVersion}/signature/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 + } + } + ).then((json) => { + const args = { + json, + params + }; + return args; + }); + }, + + setWorldImage(params) { + const worldStore = useWorldStore(); + return request(`worlds/${params.id}`, { + method: 'PUT', + params + }).then((json) => { + const args = { + json, + params + }; + args.ref = worldStore.applyWorld(json); + return args; + }); + }, + + getAvatarImages(params) { + return request(`file/${params.fileId}`, { + method: 'GET' + }).then((json) => { + const args = { + json, + params + }; + return args; + }); + }, + + getWorldImages(params) { + return request(`file/${params.fileId}`, { + method: 'GET', + params + }).then((json) => { + const args = { + json, + params + }; + return args; + }); + } +}; + +export default imageReq; diff --git a/src/api/index.js b/src/api/index.js index 25a49272..36c30f2c 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -21,6 +21,7 @@ import groupRequest from './group'; import authRequest from './auth'; import inventoryRequest from './inventory'; import propRequest from './prop'; +import imageRequest from './image'; window.request = { request, @@ -40,7 +41,8 @@ window.request = { authRequest, groupRequest, inventoryRequest, - propRequest + propRequest, + imageRequest }; export { @@ -61,5 +63,6 @@ export { authRequest, groupRequest, inventoryRequest, - propRequest + propRequest, + imageRequest }; diff --git a/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue b/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue index e87a4d3d..3b770cc2 100644 --- a/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue +++ b/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue @@ -35,7 +35,7 @@
- +
@@ -45,18 +45,21 @@ import { ElMessage } from 'element-plus'; import { Upload } from '@element-plus/icons-vue'; import { storeToRefs } from 'pinia'; - import { computed, ref } from 'vue'; + import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; - import { avatarRequest } from '../../../api'; + import { avatarRequest, imageRequest } from '../../../api'; import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; import { useAvatarStore } from '../../../stores'; + import { $throw } from '../../../service/request'; + import { AppDebug } from '../../../service/appConfig'; + import { extractFileId } from '../../../shared/utils'; const { t } = useI18n(); const { avatarDialog } = storeToRefs(useAvatarStore()); const { applyAvatar } = useAvatarStore(); - const props = defineProps({ + defineProps({ changeAvatarImageDialogVisible: { type: Boolean, required: true @@ -68,7 +71,14 @@ }); const changeAvatarImageDialogLoading = ref(false); - const currentImageUrl = computed(() => props.previousImageUrl); + const avatarImage = ref({ + base64File: '', + fileMd5: '', + base64SignatureFile: '', + signatureMd5: '', + fileId: '', + avatarId: '' + }); const emit = defineEmits(['update:changeAvatarImageDialogVisible', 'update:previousImageUrl']); @@ -106,9 +116,15 @@ try { const base64File = await resizeImageToFitLimits(btoa(r.result.toString())); // 10MB - await initiateUpload(base64File); + if (LINUX) { + // use new website upload process on Linux, we're missing the needed libraries for Unity method + // website method clears avatar name and is missing world image uploading + await initiateUpload(base64File); + return; + } + await initiateUploadLegacy(base64File, file); } catch (error) { - console.error('Avatar image upload process failed:', error); + console.error('avatar image upload process failed:', error); } finally { finalize(); } @@ -123,6 +139,164 @@ } } + async function initiateUploadLegacy(base64File, file) { + const fileMd5 = await AppApi.MD5File(base64File); + const fileSizeInBytes = parseInt(file.size, 10); + const base64SignatureFile = await AppApi.SignFile(base64File); + const signatureMd5 = await AppApi.MD5File(base64SignatureFile); + const signatureSizeInBytes = parseInt(await AppApi.FileLength(base64SignatureFile), 10); + const avatarId = avatarDialog.value.id; + const { imageUrl } = avatarDialog.value.ref; + const fileId = extractFileId(imageUrl); + avatarImage.value = { + base64File, + fileMd5, + base64SignatureFile, + signatureMd5, + fileId, + avatarId + }; + const params = { + fileMd5, + fileSizeInBytes, + signatureMd5, + signatureSizeInBytes + }; + const res = await imageRequest.uploadAvatarImage(params, fileId); + return avatarImageInit(res); + } + + async function avatarImageInit(args) { + const fileId = args.json.id; + const fileVersion = args.json.versions[args.json.versions.length - 1].version; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadAvatarImageFileStart(params); + return avatarImageFileStart(res); + } + + async function avatarImageFileStart(args) { + const { url } = args.json; + const { fileId, fileVersion } = args.params; + const params = { + url, + fileId, + fileVersion + }; + return uploadAvatarImageFileAWS(params); + } + + async function uploadAvatarImageFileAWS(params) { + const json = await webApiService.execute({ + url: params.url, + uploadFilePUT: true, + fileData: avatarImage.value.base64File, + fileMIME: 'image/png', + headers: { + 'Content-MD5': avatarImage.value.fileMd5 + } + }); + + if (json.status !== 200) { + changeAvatarImageDialogLoading.value = false; + $throw(json.status, 'avatar image upload failed', params.url); + } + const args = { + json, + params + }; + return avatarImageFileAWS(args); + } + + async function avatarImageFileAWS(args) { + const { fileId, fileVersion } = args.params; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadAvatarImageFileFinish(params); + return avatarImageFileFinish(res); + } + + async function avatarImageFileFinish(args) { + const { fileId, fileVersion } = args.params; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadAvatarImageSigStart(params); + return avatarImageSigStart(res); + } + + async function avatarImageSigStart(args) { + const { url } = args.json; + const { fileId, fileVersion } = args.params; + const params = { + url, + fileId, + fileVersion + }; + return uploadAvatarImageSigAWS(params); + } + + async function uploadAvatarImageSigAWS(params) { + const json = await webApiService.execute({ + url: params.url, + uploadFilePUT: true, + fileData: avatarImage.value.base64SignatureFile, + fileMIME: 'application/x-rsync-signature', + headers: { + 'Content-MD5': avatarImage.value.signatureMd5 + } + }); + + if (json.status !== 200) { + changeAvatarImageDialogLoading.value = false; + $throw(json.status, 'avatar image upload failed', params.url); + } + const args = { + json, + params + }; + return avatarImageSigAWS(args); + } + + async function avatarImageSigAWS(args) { + const { fileId, fileVersion } = args.params; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadAvatarImageSigFinish(params); + return avatarImageSigFinish(res); + } + async function avatarImageSigFinish(args) { + const { fileId, fileVersion } = args.params; + const params = { + id: avatarImage.value.avatarId, + imageUrl: `${AppDebug.endpointDomain}/file/${fileId}/${fileVersion}/file` + }; + const res = await imageRequest.setAvatarImage(params); + return avatarImageSet(res); + } + + function avatarImageSet(args) { + changeAvatarImageDialogLoading.value = false; + if (args.json.imageUrl === args.params.imageUrl) { + ElMessage({ + message: t('message.avatar.image_changed'), + type: 'success' + }); + emit('update:previousImageUrl', args.json.imageUrl); + } else { + $throw(0, 'avatar image change failed', args.params.imageUrl); + } + } + + // ------------ Upload Process End ------------ + async function initiateUpload(base64File) { const args = await avatarRequest.uploadAvatarImage(base64File); const fileUrl = args.json.versions[args.json.versions.length - 1].file.url; @@ -131,8 +305,8 @@ imageUrl: fileUrl }); const ref = applyAvatar(avatarArgs.json); - emit('update:previousImageUrl', ref.imageUrl); changeAvatarImageDialogLoading.value = false; + emit('update:previousImageUrl', ref.imageUrl); ElMessage({ message: t('message.avatar.image_changed'), type: 'success' diff --git a/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue b/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue index 25ce239c..e826dea6 100644 --- a/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue +++ b/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue @@ -47,9 +47,12 @@ import { storeToRefs } from 'pinia'; import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; - import { worldRequest } from '../../../api'; + import { worldRequest, imageRequest } from '../../../api'; import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; import { useWorldStore } from '../../../stores'; + import { $throw } from '../../../service/request'; + import { AppDebug } from '../../../service/appConfig'; + import { extractFileId } from '../../../shared/utils'; const { t } = useI18n(); @@ -68,6 +71,14 @@ }); const changeWorldImageDialogLoading = ref(false); + const worldImage = ref({ + base64File: '', + fileMd5: '', + base64SignatureFile: '', + signatureMd5: '', + fileId: '', + worldId: '' + }); const emit = defineEmits(['update:changeWorldImageDialogVisible', 'update:previousImageUrl']); @@ -105,7 +116,8 @@ try { const base64File = await resizeImageToFitLimits(btoa(r.result.toString())); // 10MB - await initiateUpload(base64File); + await initiateUploadLegacy(base64File, file); + // await initiateUpload(base64File); } catch (error) { console.error('World image upload process failed:', error); } finally { @@ -122,6 +134,164 @@ } } + async function initiateUploadLegacy(base64File, file) { + const fileMd5 = await AppApi.MD5File(base64File); + const fileSizeInBytes = parseInt(file.size, 10); + const base64SignatureFile = await AppApi.SignFile(base64File); + const signatureMd5 = await AppApi.MD5File(base64SignatureFile); + const signatureSizeInBytes = parseInt(await AppApi.FileLength(base64SignatureFile), 10); + const worldId = worldDialog.value.id; + const { imageUrl } = worldDialog.value.ref; + const fileId = extractFileId(imageUrl); + worldImage.value = { + base64File, + fileMd5, + base64SignatureFile, + signatureMd5, + fileId, + worldId + }; + const params = { + fileMd5, + fileSizeInBytes, + signatureMd5, + signatureSizeInBytes + }; + const res = await imageRequest.uploadWorldImage(params, fileId); + return worldImageInit(res); + } + + async function worldImageInit(args) { + const fileId = args.json.id; + const fileVersion = args.json.versions[args.json.versions.length - 1].version; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadWorldImageFileStart(params); + return worldImageFileStart(res); + } + + async function worldImageFileStart(args) { + const { url } = args.json; + const { fileId, fileVersion } = args.params; + const params = { + url, + fileId, + fileVersion + }; + return uploadWorldImageFileAWS(params); + } + + async function uploadWorldImageFileAWS(params) { + const json = await webApiService.execute({ + url: params.url, + uploadFilePUT: true, + fileData: worldImage.value.base64File, + fileMIME: 'image/png', + headers: { + 'Content-MD5': worldImage.value.fileMd5 + } + }); + + if (json.status !== 200) { + changeWorldImageDialogLoading.value = false; + $throw(json.status, 'World image upload failed', params.url); + } + const args = { + json, + params + }; + return worldImageFileAWS(args); + } + + async function worldImageFileAWS(args) { + const { fileId, fileVersion } = args.params; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadWorldImageFileFinish(params); + return worldImageFileFinish(res); + } + + async function worldImageFileFinish(args) { + const { fileId, fileVersion } = args.params; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadWorldImageSigStart(params); + return worldImageSigStart(res); + } + + async function worldImageSigStart(args) { + const { url } = args.json; + const { fileId, fileVersion } = args.params; + const params = { + url, + fileId, + fileVersion + }; + return uploadWorldImageSigAWS(params); + } + + async function uploadWorldImageSigAWS(params) { + const json = await webApiService.execute({ + url: params.url, + uploadFilePUT: true, + fileData: worldImage.value.base64SignatureFile, + fileMIME: 'application/x-rsync-signature', + headers: { + 'Content-MD5': worldImage.value.signatureMd5 + } + }); + + if (json.status !== 200) { + changeWorldImageDialogLoading.value = false; + $throw(json.status, 'World image upload failed', params.url); + } + const args = { + json, + params + }; + return worldImageSigAWS(args); + } + + async function worldImageSigAWS(args) { + const { fileId, fileVersion } = args.params; + const params = { + fileId, + fileVersion + }; + const res = await imageRequest.uploadWorldImageSigFinish(params); + return worldImageSigFinish(res); + } + async function worldImageSigFinish(args) { + const { fileId, fileVersion } = args.params; + const params = { + id: worldImage.value.worldId, + imageUrl: `${AppDebug.endpointDomain}/file/${fileId}/${fileVersion}/file` + }; + const res = await imageRequest.setWorldImage(params); + return worldImageSet(res); + } + + function worldImageSet(args) { + changeWorldImageDialogLoading.value = false; + if (args.json.imageUrl === args.params.imageUrl) { + ElMessage({ + message: t('message.world.image_changed'), + type: 'success' + }); + emit('update:previousImageUrl', args.json.imageUrl); + } else { + $throw(0, 'World image change failed', args.params.imageUrl); + } + } + + // ------------ Upload Process End ------------ + async function initiateUpload(base64File) { const args = await worldRequest.uploadWorldImage(base64File); const fileUrl = args.json.versions[args.json.versions.length - 1].file.url; diff --git a/src/components/dialogs/WorldDialog/WorldDialog.vue b/src/components/dialogs/WorldDialog/WorldDialog.vue index 017ab2d7..7650efc6 100644 --- a/src/components/dialogs/WorldDialog/WorldDialog.vue +++ b/src/components/dialogs/WorldDialog/WorldDialog.vue @@ -283,7 +283,7 @@ {{ t('dialog.world.actions.change_allowed_video_player_domains') }} - + {{ t('dialog.world.actions.change_image') }} WINDOWS); + const memo = computed({ get() { return worldDialog.value.memo; @@ -966,7 +968,7 @@ treeData.value = []; } - function showChangeAvatarImageDialog() { + function showChangeWorldImageDialog() { const { imageUrl } = worldDialog.value.ref; previousImageUrl.value = imageUrl; changeWorldImageDialogVisible.value = true; @@ -1120,7 +1122,7 @@ openExternalLink(replaceVrcPackageUrl(worldDialog.value.ref.unityPackageUrl)); break; case 'Change Image': - showChangeAvatarImageDialog(); + showChangeWorldImageDialog(); break; case 'Refresh': showWorldDialog(D.id); diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index bfbfa88d..20773262 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -186,7 +186,6 @@ declare global { SetUserAgent(): Promise; // Common Functions - MD5File(blob: string): Promise; GetColourFromUserID(userId: string): Promise; OpenLink(url: string): Promise; GetLaunchCommand(): Promise; @@ -207,6 +206,11 @@ declare global { GetFileBase64(path: string): Promise; TryOpenInstanceInVrc(launchUrl: string): Promise; + // Image Upload (Cef Only) + MD5File(blob: string): Promise; + SignFile(blob: string): Promise; + FileLength(blob: string): Promise; + // Folders GetVRChatAppDataLocation(): Promise; GetVRChatPhotosLocation(): Promise;