mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-12 11:23:52 +02:00
* Update Vue devtools
* upgrade vue pinia element-plus vue-i18n, add vite
* fix: i18n
* global components
* change v-deep
* upgrade vue-lazyload
* data table
* update enlint and safe-dialog
* package.json and vite.config.js
* el-icon
* el-message
* vue 2 -> vue3 migration changes
* $pinia
* dialog
* el-popover slot
* lint
* chore
* slot
* scss
* remote state access
* misc
* jsconfig
* el-button size mini -> small
* :model-value
* ElMessageBox
* datatable
* remove v-lazyload
* template #dropdown
* mini -> small
* css
* byebye hideTooltips
* use sass-embedded
* Update SQLite, remove unneeded libraries
* Fix shift remove local avatar favorites
* Electron arm64
* arm64 support
* bye pug
* f-word vite hah
* misc
* remove safe dialog component
* Add self invite to launch dialog
* Fix errors
* Icons 1
* improve localfavorite loading performance
* improve favorites world item performance
* dialog visibility changes for Element Plus
* clear element plus error
* import performance
* revert App.vue hah
* hah
* Revert "Add self invite to launch dialog"
This reverts commit 4801cfad58.
* Toggle self invite/open in-game
* Self invite on launch dialog
* el-button icon
* el-icon
* fix user dialog tab switching logic
* fix PlayerList
* Formatting changes
* More icons
* Fix friend log table
* loading margin
* fix markdown
* fix world dialog tab switching issue
* Fixes and formatting
* fix: global i18n.t export
* fix favorites world tab not working
* Create instance, displayName
* Remove group members sort by userId
* Fix loading dialog tabs on swtich
* Star
* charts console.warn
* wip: fix charts
* wip: fix charts
* wip: charts composables
* fix favorite item tooltip warning
* Fixes and formatting
* Clean up image dialogs
* Remove unused method
* Fix platform/size border
* Fix platform/size border
* $vr
* fix friendExportDialogVisible binding
* ElMessageBox and Settings
* Login formatting
* Rename VR overlay query
* Fix image popover and userdialog badges
* Formatting
* Big buttons
* Fixes, update Cef
* Fix gameLog table nav buttons jumping around while using nav buttons
* Fix z-index
* vr overlay
* vite input add theme
* defineAsyncComponent
* ISO 639-1
* fix i18n
* clean t
* Formatting, fix calendar, rotate arrows
* Show user status when user is offline
* Fix VR overlay
* fix theme and clean up
* split InstanceActivity
* tweak
* Fix VR overlay formatting
* fix scss var
* AppDebug hahahaha
* Years
* remove reactive
* improve perf
* state hah…
* fix user rendering poblems when user object is not yet loaded
* improve perf
* Update avatar/world image uploader, licenses, remove previous images dialog (old images are now deleted)
* improve perf 1
* Suppress stray errors
* fix traveling location display issue
* Fix empty instance creator
* improve friend list refresh performance
* fix main charts
* fix chart
* Fix darkmode
* Fix avatar dialog tags
---------
Co-authored-by: pa <maplenagisa@gmail.com>
394 lines
13 KiB
JavaScript
394 lines
13 KiB
JavaScript
import Noty from 'noty';
|
|
import { ElMessageBox, ElMessage } from 'element-plus';
|
|
import { i18n } from '../plugin/i18n';
|
|
import { statusCodes } from '../shared/constants/api.js';
|
|
import { escapeTag } from '../shared/utils';
|
|
import {
|
|
useAuthStore,
|
|
useAvatarStore,
|
|
useNotificationStore,
|
|
useUpdateLoopStore,
|
|
useUserStore
|
|
} from '../stores';
|
|
import { AppDebug } from './appConfig.js';
|
|
import webApiService from './webapi.js';
|
|
import { watchState } from './watchState';
|
|
|
|
const pendingGetRequests = new Map();
|
|
export let failedGetRequests = new Map();
|
|
|
|
const t = i18n.global.t;
|
|
|
|
/**
|
|
* @template T
|
|
* @param {string} endpoint
|
|
* @param {RequestInit & { params?: any }} [options]
|
|
* @returns {Promise<T>}
|
|
*/
|
|
export function request(endpoint, options) {
|
|
const userStore = useUserStore();
|
|
const avatarStore = useAvatarStore();
|
|
const authStore = useAuthStore();
|
|
const notificationStore = useNotificationStore();
|
|
const updateLoopStore = useUpdateLoopStore();
|
|
if (
|
|
!watchState.isLoggedIn &&
|
|
endpoint.startsWith('/auth') &&
|
|
endpoint !== 'config'
|
|
) {
|
|
throw `API request blocked while logged out: ${endpoint}`;
|
|
}
|
|
let req;
|
|
const init = {
|
|
url: `${AppDebug.endpointDomain}/${endpoint}`,
|
|
method: 'GET',
|
|
...options
|
|
};
|
|
const { params } = init;
|
|
if (init.method === 'GET') {
|
|
// don't retry recent 404/403
|
|
if (failedGetRequests.has(endpoint)) {
|
|
const lastRun = failedGetRequests.get(endpoint);
|
|
if (lastRun >= Date.now() - 900000) {
|
|
// 15mins
|
|
$throw(
|
|
-1,
|
|
t('api.error.message.403_404_bailing_request'),
|
|
endpoint
|
|
);
|
|
}
|
|
failedGetRequests.delete(endpoint);
|
|
}
|
|
// transform body to url
|
|
if (params === Object(params)) {
|
|
const url = new URL(init.url);
|
|
const { searchParams } = url;
|
|
for (const key in params) {
|
|
searchParams.set(key, params[key]);
|
|
}
|
|
init.url = url.toString();
|
|
}
|
|
// merge requests
|
|
req = pendingGetRequests.get(init.url);
|
|
if (typeof req !== 'undefined') {
|
|
if (req.time >= Date.now() - 10000) {
|
|
// 10s
|
|
return req.req;
|
|
}
|
|
pendingGetRequests.delete(init.url);
|
|
}
|
|
} else if (
|
|
init.uploadImage ||
|
|
init.uploadFilePUT ||
|
|
init.uploadImageLegacy
|
|
) {
|
|
// nothing
|
|
} else {
|
|
init.headers = {
|
|
'Content-Type': 'application/json;charset=utf-8',
|
|
...init.headers
|
|
};
|
|
init.body = params === Object(params) ? JSON.stringify(params) : '{}';
|
|
}
|
|
req = webApiService
|
|
.execute(init)
|
|
.catch((err) => {
|
|
$throw(0, err, endpoint);
|
|
})
|
|
.then((response) => {
|
|
if (
|
|
!watchState.isLoggedIn &&
|
|
endpoint.startsWith('/auth') &&
|
|
endpoint !== 'config'
|
|
) {
|
|
throw `API request blocked while logged out: ${endpoint}`;
|
|
}
|
|
if (!response.data) {
|
|
if (AppDebug.debugWebRequests) {
|
|
console.log(init, 'no data', response);
|
|
}
|
|
return response;
|
|
}
|
|
try {
|
|
response.data = JSON.parse(response.data);
|
|
if (AppDebug.debugWebRequests) {
|
|
console.log(init, 'parsed data', response.data);
|
|
}
|
|
return response;
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
if (response.status === 200) {
|
|
$throw(
|
|
0,
|
|
t('api.error.message.invalid_json_response'),
|
|
endpoint
|
|
);
|
|
}
|
|
if (
|
|
response.status === 429 &&
|
|
init.url.endsWith('/instances/groups')
|
|
) {
|
|
updateLoopStore.nextGroupInstanceRefresh = 120; // 1min
|
|
$throw(429, t('api.status_code.429'), endpoint);
|
|
}
|
|
if (response.status === 504 || response.status === 502) {
|
|
// ignore expected API errors
|
|
$throw(response.status, response.data || '', endpoint);
|
|
}
|
|
$throw(
|
|
response.status,
|
|
response.data || response.statusText,
|
|
endpoint
|
|
);
|
|
})
|
|
.then(({ data, status }) => {
|
|
if (status === 200) {
|
|
if (!data) {
|
|
return data;
|
|
}
|
|
let text = '';
|
|
if (data.success === Object(data.success)) {
|
|
text = data.success.message;
|
|
} else if (data.OK === String(data.OK)) {
|
|
text = data.OK;
|
|
}
|
|
if (text) {
|
|
new Noty({
|
|
type: 'success',
|
|
text: escapeTag(text)
|
|
}).show();
|
|
}
|
|
return data;
|
|
}
|
|
if (status === 401) {
|
|
if (data.error?.message === '"Missing Credentials"') {
|
|
authStore.handleAutoLogin();
|
|
$throw(
|
|
401,
|
|
t('api.error.message.missing_credentials'),
|
|
endpoint
|
|
);
|
|
} else if (
|
|
data.error.message === '"Unauthorized"' &&
|
|
endpoint !== 'auth/user'
|
|
) {
|
|
// trigger 2FA dialog }
|
|
if (!authStore.twoFactorAuthDialogVisible) {
|
|
userStore.getCurrentUser();
|
|
}
|
|
$throw(401, t('api.status_code.401'), endpoint);
|
|
}
|
|
}
|
|
if (status === 403 && endpoint === 'config') {
|
|
ElMessageBox.alert(
|
|
t('api.error.message.vpn_in_use'),
|
|
`403 ${t('api.error.message.login_error')}`
|
|
);
|
|
authStore.handleLogoutEvent();
|
|
$throw(403, endpoint);
|
|
}
|
|
if (
|
|
init.method === 'GET' &&
|
|
status === 404 &&
|
|
endpoint.startsWith('avatars/')
|
|
) {
|
|
ElMessage({
|
|
message: t('message.api_handler.avatar_private_or_deleted'),
|
|
type: 'error'
|
|
});
|
|
avatarStore.avatarDialog.visible = false;
|
|
$throw(404, data.error?.message || '', endpoint);
|
|
}
|
|
if (status === 404 && endpoint.endsWith('/persist/exists')) {
|
|
return false;
|
|
}
|
|
if (
|
|
init.method === 'GET' &&
|
|
(status === 404 || status === 403) &&
|
|
!endpoint.startsWith('auth/user')
|
|
) {
|
|
failedGetRequests.set(endpoint, Date.now());
|
|
}
|
|
if (
|
|
init.method === 'GET' &&
|
|
status === 404 &&
|
|
endpoint.startsWith('users/') &&
|
|
endpoint.split('/').length - 1 === 1
|
|
) {
|
|
$throw(404, data.error?.message || '', endpoint);
|
|
}
|
|
if (
|
|
status === 404 &&
|
|
endpoint.startsWith('invite/') &&
|
|
init.inviteId
|
|
) {
|
|
notificationStore.expireNotification(init.inviteId);
|
|
}
|
|
if (status === 403 && endpoint.startsWith('invite/myself/to/')) {
|
|
$throw(403, data.error?.message || '', endpoint);
|
|
}
|
|
if (data && data.error === Object(data.error)) {
|
|
$throw(
|
|
data.error.status_code || status,
|
|
data.error.message,
|
|
endpoint
|
|
);
|
|
} else if (data && typeof data.error === 'string') {
|
|
$throw(data.status_code || status, data.error, endpoint);
|
|
}
|
|
$throw(status, data, endpoint);
|
|
});
|
|
if (init.method === 'GET') {
|
|
req.finally(() => {
|
|
pendingGetRequests.delete(init.url);
|
|
});
|
|
pendingGetRequests.set(init.url, {
|
|
req,
|
|
time: Date.now()
|
|
});
|
|
}
|
|
return req;
|
|
}
|
|
|
|
/**
|
|
* @param {number} code
|
|
* @param {string|object} [error]
|
|
* @param {string} [endpoint]
|
|
*/
|
|
export function $throw(code, error, endpoint) {
|
|
let message = [];
|
|
if (code > 0) {
|
|
const status = statusCodes[code];
|
|
if (typeof status === 'undefined') {
|
|
message.push(`${code}`);
|
|
} else {
|
|
const codeText = t(`api.status_code.${code}`);
|
|
message.push(`${code} ${codeText}`);
|
|
}
|
|
}
|
|
if (typeof error !== 'undefined') {
|
|
message.push(
|
|
`${t('api.error.message.error_message')}: ${typeof error === 'string' ? error : JSON.stringify(error)}`
|
|
);
|
|
}
|
|
if (typeof endpoint !== 'undefined') {
|
|
message.push(
|
|
`${t('api.error.message.endpoint')}: "${typeof endpoint === 'string' ? endpoint : JSON.stringify(endpoint)}"`
|
|
);
|
|
}
|
|
const text = message.map((s) => escapeTag(s)).join('<br>');
|
|
let ignoreError = false;
|
|
if (
|
|
(code === 404 || code === -1) &&
|
|
endpoint.split('/').length === 2 &&
|
|
(endpoint.startsWith('users/') ||
|
|
endpoint.startsWith('worlds/') ||
|
|
endpoint.startsWith('avatars/') ||
|
|
endpoint.startsWith('file/'))
|
|
) {
|
|
ignoreError = true;
|
|
}
|
|
if (endpoint.startsWith('analysis/')) {
|
|
ignoreError = true;
|
|
}
|
|
if (text.length && !ignoreError) {
|
|
if (AppDebug.errorNoty) {
|
|
AppDebug.errorNoty.close();
|
|
}
|
|
AppDebug.errorNoty = new Noty({
|
|
type: 'error',
|
|
text
|
|
});
|
|
AppDebug.errorNoty.show();
|
|
}
|
|
const e = new Error(text);
|
|
e.status = code;
|
|
e.endpoint = endpoint;
|
|
throw e;
|
|
}
|
|
|
|
/**
|
|
* Processes data in bulk by making paginated requests until all data is fetched or limits are reached.
|
|
*
|
|
* @async
|
|
* @function processBulk
|
|
* @param {object} options - Configuration options for bulk processing
|
|
* @param {function} options.fn - The function to call for each batch request. Must return a result with a 'json' property containing an array
|
|
* @param {object} [options.params={}] - Parameters to pass to the function. Will be modified to include pagination
|
|
* @param {number} [options.N=-1] - Maximum number of items to fetch. -1 for unlimited, 0 for fetch until page size not met
|
|
* @param {string} [options.limitParam='n'] - The parameter name used for page size in the request
|
|
* @param {function} [options.handle] - Callback function to handle each batch result
|
|
* @param {function} [options.done] - Callback function called when processing is complete. Receives boolean indicating success
|
|
* @returns {Promise<void>} Promise that resolves when bulk processing is complete
|
|
*
|
|
* @example
|
|
* await processBulk({
|
|
* fn: fetchUsers,
|
|
* params: { n: 50 },
|
|
* N: 200,
|
|
* handle: (result) => console.log(`Fetched ${result.json.length} users`),
|
|
* done: (success) => console.log(success ? 'Complete' : 'Failed')
|
|
* });
|
|
*/
|
|
export async function processBulk(options) {
|
|
const {
|
|
fn,
|
|
params: rawParams = {},
|
|
N = -1,
|
|
limitParam = 'n',
|
|
handle,
|
|
done
|
|
} = options;
|
|
|
|
if (typeof fn !== 'function') {
|
|
return;
|
|
}
|
|
|
|
const params = { ...rawParams };
|
|
if (typeof params.offset !== 'number') {
|
|
params.offset = 0;
|
|
}
|
|
const pageSize = params[limitParam];
|
|
|
|
let totalFetched = 0;
|
|
|
|
try {
|
|
while (true) {
|
|
const result = await fn(params);
|
|
const batchSize = result.json.length;
|
|
|
|
if (typeof handle === 'function') {
|
|
handle(result);
|
|
}
|
|
if (batchSize === 0) {
|
|
break;
|
|
}
|
|
|
|
if (N > 0) {
|
|
totalFetched += batchSize;
|
|
if (totalFetched >= N) {
|
|
break;
|
|
}
|
|
} else if (N === 0) {
|
|
if (batchSize < pageSize) {
|
|
break;
|
|
}
|
|
totalFetched += batchSize;
|
|
} else {
|
|
totalFetched += batchSize;
|
|
}
|
|
params.offset += batchSize;
|
|
}
|
|
|
|
if (typeof done === 'function') {
|
|
done(true);
|
|
}
|
|
} catch (err) {
|
|
console.error('Bulk processing error:', err);
|
|
if (typeof done === 'function') {
|
|
done(false);
|
|
}
|
|
}
|
|
}
|