refactor: app.js (#1291)

* refactor: frontend

* Fix avatar gallery sort

* Update .NET dependencies

* Update npm dependencies

electron v37.1.0

* bulkRefreshFriends

* fix dark theme

* Remove crowdin

* Fix config.json dialog not updating

* VRCX log file fixes & add Cef log

* Remove SharedVariable, fix startup

* Revert init theme change

* Logging date not working? Fix WinformThemer designer error

* Add Cef request hander, no more escaping main page

* clean

* fix

* fix

* clean

* uh

* Apply thememode at startup, fixes random user colours

* Split database into files

* Instance info remove empty lines

* Open external VRC links with VRCX

* Electron fixes

* fix userdialog style

* ohhhh

* fix store

* fix store

* fix: load all group members after kicking a user

* fix: world dialog favorite button style

* fix: Clear VRCX Cache Timer input value

* clean

* Fix VR overlay

* Fix VR overlay 2

* Fix Discord discord rich presence for RPC worlds

* Clean up age verified user tags

* Fix playerList being occupied after program reload

* no `this`

* Fix login stuck loading

* writable: false

* Hide dialogs on logout

* add flush sync option

* rm LOGIN event

* rm LOGOUT event

* remove duplicate event listeners

* remove duplicate event listeners

* clean

* remove duplicate event listeners

* clean

* fix theme style

* fix t

* clearable

* clean

* fix ipcEvent

* Small changes

* Popcorn Palace support

* Remove checkActiveFriends

* Clean up

* Fix dragEnterCef

* Block API requests when not logged in

* Clear state on login & logout

* Fix worldDialog instances not updating

* use <script setup>

* Fix avatar change event, CheckGameRunning at startup

* Fix image dragging

* fix

* Remove PWI

* fix updateLoop

* add webpack-dev-server to dev environment

* rm unnecessary chunks

* use <script setup>

* webpack-dev-server changes

* use <script setup>

* use <script setup>

* Fix UGC text size

* Split login event

* t

* use <script setup>

* fix

* Update .gitignore and enable checkJs in jsconfig

* fix i18n t

* use <script setup>

* use <script setup>

* clean

* global types

* fix

* use checkJs for debugging

* Add watchState for login watchers

* fix .vue template

* type fixes

* rm Vue.filter

* Cef v138.0.170, VC++ 2022

* Settings fixes

* Remove 'USER:CURRENT'

* clean up 2FA callbacks

* remove userApply

* rm i18n import

* notification handling to use notification store methods

* refactor favorite handling to use favorite store methods and clean up event emissions

* refactor moderation handling to use dedicated functions for player moderation events

* refactor friend handling to use dedicated functions for friend events

* Fix program startup, move lang init

* Fix friend state

* Fix status change error

* Fix user notes diff

* fix

* rm group event

* rm auth event

* rm avatar event

* clean

* clean

* getUser

* getFriends

* getFavoriteWorlds, getFavoriteAvatars

* AvatarGalleryUpload btn style & package.json update

* Fix friend requests

* Apply user

* Apply world

* Fix note diff

* Fix VR overlay

* Fixes

* Update build scripts

* Apply avatar

* Apply instance

* Apply group

* update hidden VRC+ badge

* Fix sameInstance "private"

* fix 502/504 API errors

* fix 502/504 API errors

* clean

* Fix friend in same instance on orange showing twice in friends list

* Add back in broken friend state repair methods

* add types

---------

Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
pa
2025-07-14 12:00:08 +09:00
committed by GitHub
parent 952fd77ed5
commit f4f78bb5ec
323 changed files with 47745 additions and 43326 deletions

377
src/service/request.js Normal file
View File

@@ -0,0 +1,377 @@
import Noty from 'noty';
import { $app } from '../app.js';
import { t } from '../plugin';
import { statusCodes } from '../shared/constants/api.js';
import { escapeTag } from '../shared/utils';
import {
useAuthStore,
useAvatarStore,
useNotificationStore,
useUpdateLoopStore,
useUserStore
} from '../stores';
import { AppGlobal } from './appConfig.js';
import webApiService from './webapi.js';
import { watchState } from './watchState';
const pendingGetRequests = new Map();
export let failedGetRequests = new Map();
/**
* @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: `${AppGlobal.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(
0,
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 (AppGlobal.debugWebRequests) {
console.log(init, 'no data', response);
}
return response;
}
try {
response.data = JSON.parse(response.data);
if (AppGlobal.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') {
$app.$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/')
) {
$app.$message({
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>');
if (text.length) {
if (AppGlobal.errorNoty) {
AppGlobal.errorNoty.close();
}
AppGlobal.errorNoty = new Noty({
type: 'error',
text
});
AppGlobal.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);
}
}
}