mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-02 04:56:06 +02:00
rename
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
// Mock router to avoid transitive i18n.global error from columns.jsx
|
||||
vi.mock('../../plugins/router.js', () => ({
|
||||
router: { beforeEach: vi.fn(), push: vi.fn() },
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
|
||||
import { transformKey } from '../config.js';
|
||||
|
||||
describe('transformKey', () => {
|
||||
test('lowercases and prefixes with config:', () => {
|
||||
expect(transformKey('Foo')).toBe('config:foo');
|
||||
});
|
||||
|
||||
test('handles already lowercase key', () => {
|
||||
expect(transformKey('bar')).toBe('config:bar');
|
||||
});
|
||||
|
||||
test('handles key with mixed case and numbers', () => {
|
||||
expect(transformKey('MyKey123')).toBe('config:mykey123');
|
||||
});
|
||||
|
||||
test('handles empty string', () => {
|
||||
expect(transformKey('')).toBe('config:');
|
||||
});
|
||||
|
||||
test('converts non-string values via String()', () => {
|
||||
expect(transformKey(42)).toBe('config:42');
|
||||
expect(transformKey(null)).toBe('config:null');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import removeConfusables, { removeWhitespace } from '../confusables.js';
|
||||
|
||||
describe('removeConfusables', () => {
|
||||
test('returns ASCII strings unchanged (fast path)', () => {
|
||||
expect(removeConfusables('Hello World')).toBe('HelloWorld');
|
||||
});
|
||||
|
||||
test('converts circled letters to ASCII', () => {
|
||||
expect(removeConfusables('Ⓗⓔⓛⓛⓞ')).toBe('Hello');
|
||||
});
|
||||
|
||||
test('converts fullwidth letters to ASCII', () => {
|
||||
expect(removeConfusables('Hello')).toBe('Hello');
|
||||
});
|
||||
|
||||
test('converts Cyrillic confusables', () => {
|
||||
// Cyrillic А, В, С look like Latin A, B, C
|
||||
expect(removeConfusables('АВС')).toBe('ABC');
|
||||
});
|
||||
|
||||
test('handles mixed confusables and normal chars', () => {
|
||||
expect(removeConfusables('Ⓣest')).toBe('Test');
|
||||
});
|
||||
|
||||
test('strips combining marks', () => {
|
||||
// 'e' + combining acute accent → normalized then combining mark stripped → 'e'
|
||||
const input = 'e\u0301';
|
||||
const result = removeConfusables(input);
|
||||
expect(result).toBe('e');
|
||||
});
|
||||
|
||||
test('returns empty string for empty input', () => {
|
||||
expect(removeConfusables('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeWhitespace', () => {
|
||||
test('removes regular spaces', () => {
|
||||
expect(removeWhitespace('a b c')).toBe('abc');
|
||||
});
|
||||
|
||||
test('removes tabs and newlines', () => {
|
||||
expect(removeWhitespace('a\tb\nc')).toBe('abc');
|
||||
});
|
||||
|
||||
test('returns string without whitespace unchanged', () => {
|
||||
expect(removeWhitespace('abc')).toBe('abc');
|
||||
});
|
||||
|
||||
test('returns empty string for empty input', () => {
|
||||
expect(removeWhitespace('')).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import { LogWatcherService } from '../gameLog.js';
|
||||
|
||||
const svc = new LogWatcherService();
|
||||
|
||||
describe('parseRawGameLog', () => {
|
||||
test('parses location type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'location', [
|
||||
'wrld_123:456',
|
||||
'Test World'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'location',
|
||||
location: 'wrld_123:456',
|
||||
worldName: 'Test World'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses location-destination type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'location-destination', [
|
||||
'wrld_abc:789'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'location-destination',
|
||||
location: 'wrld_abc:789'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses player-joined type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'player-joined', [
|
||||
'TestUser',
|
||||
'usr_123'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'player-joined',
|
||||
displayName: 'TestUser',
|
||||
userId: 'usr_123'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses player-left type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'player-left', [
|
||||
'TestUser',
|
||||
'usr_123'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'player-left',
|
||||
displayName: 'TestUser',
|
||||
userId: 'usr_123'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses notification type', () => {
|
||||
const json = '{"type":"invite"}';
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'notification', [json]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'notification',
|
||||
json
|
||||
});
|
||||
});
|
||||
|
||||
test('parses event type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'event', ['some-event']);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'event',
|
||||
event: 'some-event'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses video-play type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'video-play', [
|
||||
'https://example.com/video.mp4',
|
||||
'Player1'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'video-play',
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
displayName: 'Player1'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses resource-load-string type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'resource-load-string', [
|
||||
'https://example.com/res'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'resource-load-string',
|
||||
resourceUrl: 'https://example.com/res'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses resource-load-image type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'resource-load-image', [
|
||||
'https://example.com/img.png'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'resource-load-image',
|
||||
resourceUrl: 'https://example.com/img.png'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses avatar-change type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'avatar-change', [
|
||||
'User1',
|
||||
'CoolAvatar'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'avatar-change',
|
||||
displayName: 'User1',
|
||||
avatarName: 'CoolAvatar'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses photon-id type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'photon-id', [
|
||||
'User1',
|
||||
'42'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'photon-id',
|
||||
displayName: 'User1',
|
||||
photonId: '42'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses screenshot type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'screenshot', [
|
||||
'/path/to/screenshot.png'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'screenshot',
|
||||
screenshotPath: '/path/to/screenshot.png'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses sticker-spawn type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'sticker-spawn', [
|
||||
'usr_abc',
|
||||
'StickerUser',
|
||||
'inv_123'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'sticker-spawn',
|
||||
userId: 'usr_abc',
|
||||
displayName: 'StickerUser',
|
||||
inventoryId: 'inv_123'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses video-sync type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'video-sync', [
|
||||
'123.456'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'video-sync',
|
||||
timestamp: '123.456'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses vrcx type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'vrcx', ['some-data']);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'vrcx',
|
||||
data: 'some-data'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses api-request type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'api-request', [
|
||||
'https://api.vrchat.cloud/api/1/users'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'api-request',
|
||||
url: 'https://api.vrchat.cloud/api/1/users'
|
||||
});
|
||||
});
|
||||
|
||||
test('parses udon-exception type', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'udon-exception', [
|
||||
'NullRef'
|
||||
]);
|
||||
expect(log).toEqual({
|
||||
dt: '2024-01-01',
|
||||
type: 'udon-exception',
|
||||
data: 'NullRef'
|
||||
});
|
||||
});
|
||||
|
||||
test('handles types with no extra fields', () => {
|
||||
for (const type of [
|
||||
'portal-spawn',
|
||||
'vrc-quit',
|
||||
'openvr-init',
|
||||
'desktop-mode'
|
||||
]) {
|
||||
const log = svc.parseRawGameLog('2024-01-01', type, []);
|
||||
expect(log).toEqual({ dt: '2024-01-01', type });
|
||||
}
|
||||
});
|
||||
|
||||
test('handles unknown type gracefully', () => {
|
||||
const log = svc.parseRawGameLog('2024-01-01', 'unknown-type', ['foo']);
|
||||
expect(log).toEqual({ dt: '2024-01-01', type: 'unknown-type' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,305 @@
|
||||
// Mock router to avoid transitive i18n.global error from columns.jsx
|
||||
vi.mock('../../plugins/router.js', () => ({
|
||||
router: { beforeEach: vi.fn(), push: vi.fn() },
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
|
||||
import {
|
||||
buildRequestInit,
|
||||
parseResponse,
|
||||
processBulk,
|
||||
shouldIgnoreError
|
||||
} from '../request.js';
|
||||
|
||||
describe('buildRequestInit', () => {
|
||||
test('builds GET request with default method', () => {
|
||||
const init = buildRequestInit('users/usr_123');
|
||||
expect(init.method).toBe('GET');
|
||||
expect(init.url).toContain('users/usr_123');
|
||||
});
|
||||
|
||||
test('serializes GET params into URL search params', () => {
|
||||
const init = buildRequestInit('users', {
|
||||
params: { n: 50, offset: 0 }
|
||||
});
|
||||
expect(init.url).toContain('n=50');
|
||||
expect(init.url).toContain('offset=0');
|
||||
});
|
||||
|
||||
test('does not set body for GET requests', () => {
|
||||
const init = buildRequestInit('users', {
|
||||
params: { n: 50 }
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
test('sets JSON content-type and body for POST', () => {
|
||||
const init = buildRequestInit('auth/login', {
|
||||
method: 'POST',
|
||||
params: { username: 'test' }
|
||||
});
|
||||
expect(init.headers['Content-Type']).toBe(
|
||||
'application/json;charset=utf-8'
|
||||
);
|
||||
expect(init.body).toBe(JSON.stringify({ username: 'test' }));
|
||||
});
|
||||
|
||||
test('sets empty body when POST has no params', () => {
|
||||
const init = buildRequestInit('auth/logout', { method: 'POST' });
|
||||
expect(init.body).toBe('{}');
|
||||
});
|
||||
|
||||
test('preserves custom headers in POST', () => {
|
||||
const init = buildRequestInit('users', {
|
||||
method: 'PUT',
|
||||
headers: { 'X-Custom': 'value' },
|
||||
params: { a: 1 }
|
||||
});
|
||||
expect(init.headers['Content-Type']).toBe(
|
||||
'application/json;charset=utf-8'
|
||||
);
|
||||
expect(init.headers['X-Custom']).toBe('value');
|
||||
});
|
||||
|
||||
test('skips body/headers for upload requests', () => {
|
||||
const init = buildRequestInit('file/upload', {
|
||||
method: 'PUT',
|
||||
uploadImage: true,
|
||||
params: { something: 1 }
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
expect(init.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
test('skips body/headers for uploadFilePUT', () => {
|
||||
const init = buildRequestInit('file/upload', {
|
||||
method: 'PUT',
|
||||
uploadFilePUT: true
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
test('skips body/headers for uploadImageLegacy', () => {
|
||||
const init = buildRequestInit('file/upload', {
|
||||
method: 'POST',
|
||||
uploadImageLegacy: true
|
||||
});
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
test('passes through extra options', () => {
|
||||
const init = buildRequestInit('test', {
|
||||
method: 'DELETE',
|
||||
inviteId: 'inv_123'
|
||||
});
|
||||
expect(init.method).toBe('DELETE');
|
||||
expect(init.inviteId).toBe('inv_123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResponse', () => {
|
||||
test('returns response unchanged when no data', () => {
|
||||
const response = { status: 200 };
|
||||
expect(parseResponse(response)).toEqual({ status: 200 });
|
||||
});
|
||||
|
||||
test('parses valid JSON data', () => {
|
||||
const response = {
|
||||
status: 200,
|
||||
data: JSON.stringify({ name: 'test' })
|
||||
};
|
||||
const result = parseResponse(response);
|
||||
expect(result.data).toEqual({ name: 'test' });
|
||||
expect(result.hasApiError).toBeUndefined();
|
||||
expect(result.parseError).toBeUndefined();
|
||||
});
|
||||
|
||||
test('detects API error in response data', () => {
|
||||
const response = {
|
||||
status: 404,
|
||||
data: JSON.stringify({
|
||||
error: { status_code: 404, message: 'Not found' }
|
||||
})
|
||||
};
|
||||
const result = parseResponse(response);
|
||||
expect(result.hasApiError).toBe(true);
|
||||
expect(result.data.error.message).toBe('Not found');
|
||||
});
|
||||
|
||||
test('flags parse error for invalid JSON', () => {
|
||||
const response = { status: 200, data: 'not valid json{{{' };
|
||||
const result = parseResponse(response);
|
||||
expect(result.parseError).toBe(true);
|
||||
expect(result.status).toBe(200);
|
||||
});
|
||||
|
||||
test('handles empty string data', () => {
|
||||
const response = { status: 200, data: '' };
|
||||
const result = parseResponse(response);
|
||||
// empty string is falsy, so treated as no data
|
||||
expect(result).toEqual({ status: 200, data: '' });
|
||||
});
|
||||
|
||||
test('handles null data', () => {
|
||||
const response = { status: 200, data: null };
|
||||
const result = parseResponse(response);
|
||||
expect(result).toEqual({ status: 200, data: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldIgnoreError', () => {
|
||||
test.each([
|
||||
[404, 'users/usr_123'],
|
||||
[404, 'worlds/wrld_123'],
|
||||
[404, 'avatars/avtr_123'],
|
||||
[404, 'groups/grp_123'],
|
||||
[404, 'file/file_123'],
|
||||
[-1, 'users/usr_123']
|
||||
])('ignores %i for single-segment resource %s', (code, endpoint) => {
|
||||
expect(shouldIgnoreError(code, endpoint)).toBe(true);
|
||||
});
|
||||
|
||||
test('does NOT ignore nested resource paths', () => {
|
||||
expect(shouldIgnoreError(404, 'users/usr_123/friends')).toBe(false);
|
||||
});
|
||||
|
||||
test('does NOT ignore 403 for resource lookups', () => {
|
||||
expect(shouldIgnoreError(403, 'users/usr_123')).toBe(false);
|
||||
});
|
||||
|
||||
test.each([403, 404, -1])('ignores %i for instances/ endpoints', (code) => {
|
||||
expect(shouldIgnoreError(code, 'instances/wrld_123:456')).toBe(true);
|
||||
});
|
||||
|
||||
test('ignores any code for analysis/ endpoints', () => {
|
||||
expect(shouldIgnoreError(500, 'analysis/something')).toBe(true);
|
||||
expect(shouldIgnoreError(200, 'analysis/data')).toBe(true);
|
||||
});
|
||||
|
||||
test.each([403, -1])('ignores %i for /mutuals endpoints', (code) => {
|
||||
expect(shouldIgnoreError(code, 'users/usr_123/mutuals')).toBe(true);
|
||||
});
|
||||
|
||||
test('does NOT ignore 404 for /mutuals', () => {
|
||||
expect(shouldIgnoreError(404, 'users/usr_123/mutuals')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for unmatched patterns', () => {
|
||||
expect(shouldIgnoreError(500, 'auth/login')).toBe(false);
|
||||
expect(shouldIgnoreError(200, 'config')).toBe(false);
|
||||
});
|
||||
|
||||
test('handles undefined endpoint', () => {
|
||||
expect(shouldIgnoreError(404, undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processBulk', () => {
|
||||
test('fetches all pages until empty batch', async () => {
|
||||
const pages = [{ json: [1, 2, 3] }, { json: [4, 5] }, { json: [] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const handle = vi.fn();
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, handle, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(3);
|
||||
expect(handle).toHaveBeenCalledTimes(3);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('stops when N > 0 limit is reached', async () => {
|
||||
const fn = vi.fn(() => Promise.resolve({ json: [1, 2, 3] }));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, N: 5, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('stops when N = 0 and batch < pageSize', async () => {
|
||||
const pages = [{ json: [1, 2, 3] }, { json: [4] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, N: 0, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('stops when hasNext is false', async () => {
|
||||
const fn = vi.fn(() =>
|
||||
Promise.resolve({ json: [1, 2, 3], hasNext: false })
|
||||
);
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 3 }, N: -1, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('supports result.results array format', async () => {
|
||||
const pages = [{ results: [1, 2] }, { results: [] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 5 }, done });
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('calls done(false) when fn throws', async () => {
|
||||
const fn = vi.fn(() => Promise.reject(new Error('network error')));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({ fn, params: { n: 5 }, done });
|
||||
|
||||
expect(done).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test('increments offset correctly', async () => {
|
||||
const pages = [{ json: [1, 2, 3] }, { json: [4, 5] }, { json: [] }];
|
||||
let call = 0;
|
||||
const offsets = [];
|
||||
const fn = vi.fn((params) => {
|
||||
offsets.push(params.offset);
|
||||
const result = pages[call++];
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
|
||||
await processBulk({ fn, params: { n: 3 } });
|
||||
|
||||
expect(offsets).toEqual([0, 3, 5]);
|
||||
});
|
||||
|
||||
test('returns early if fn is not a function', async () => {
|
||||
const done = vi.fn();
|
||||
await processBulk({ fn: null, done });
|
||||
expect(done).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('uses custom limitParam', async () => {
|
||||
const pages = [{ json: [1, 2] }, { json: [1] }];
|
||||
let call = 0;
|
||||
const fn = vi.fn(() => Promise.resolve(pages[call++]));
|
||||
const done = vi.fn();
|
||||
|
||||
await processBulk({
|
||||
fn,
|
||||
params: { limit: 2 },
|
||||
limitParam: 'limit',
|
||||
N: 0,
|
||||
done
|
||||
});
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
expect(done).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import security, {
|
||||
hexToUint8Array,
|
||||
stdAESKey,
|
||||
uint8ArrayToHex
|
||||
} from '../security.js';
|
||||
|
||||
describe('hexToUint8Array', () => {
|
||||
test('converts hex string to Uint8Array', () => {
|
||||
const result = hexToUint8Array('0a1bff');
|
||||
expect(result).toEqual(new Uint8Array([0x0a, 0x1b, 0xff]));
|
||||
});
|
||||
|
||||
test('converts empty-ish input', () => {
|
||||
const result = hexToUint8Array('00');
|
||||
expect(result).toEqual(new Uint8Array([0]));
|
||||
});
|
||||
|
||||
test('returns null for empty string', () => {
|
||||
expect(hexToUint8Array('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('uint8ArrayToHex', () => {
|
||||
test('converts Uint8Array to hex string', () => {
|
||||
const result = uint8ArrayToHex(new Uint8Array([0x0a, 0x1b, 0xff]));
|
||||
expect(result).toBe('0a1bff');
|
||||
});
|
||||
|
||||
test('pads single-digit hex values', () => {
|
||||
const result = uint8ArrayToHex(new Uint8Array([0, 1, 2]));
|
||||
expect(result).toBe('000102');
|
||||
});
|
||||
|
||||
test('converts empty array', () => {
|
||||
expect(uint8ArrayToHex(new Uint8Array([]))).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hex round-trip', () => {
|
||||
test('uint8Array → hex → uint8Array preserves data', () => {
|
||||
const original = new Uint8Array([0, 127, 255, 42, 1]);
|
||||
const hex = uint8ArrayToHex(original);
|
||||
const restored = hexToUint8Array(hex);
|
||||
expect(restored).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stdAESKey', () => {
|
||||
test('pads short key to 32 bytes', () => {
|
||||
const result = stdAESKey('abc');
|
||||
expect(result.length).toBe(32);
|
||||
// First 3 bytes should be 'abc'
|
||||
expect(result[0]).toBe('a'.charCodeAt(0));
|
||||
expect(result[1]).toBe('b'.charCodeAt(0));
|
||||
expect(result[2]).toBe('c'.charCodeAt(0));
|
||||
});
|
||||
|
||||
test('truncates long key to 32 bytes', () => {
|
||||
const longKey = 'a'.repeat(64);
|
||||
const result = stdAESKey(longKey);
|
||||
expect(result.length).toBe(32);
|
||||
});
|
||||
|
||||
test('32-byte key stays unchanged', () => {
|
||||
const key = 'abcdefghijklmnopqrstuvwxyz012345'; // exactly 32 chars
|
||||
const result = stdAESKey(key);
|
||||
expect(result.length).toBe(32);
|
||||
const expected = new TextEncoder().encode(key);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt / decrypt round-trip', () => {
|
||||
test('encrypts and decrypts plaintext correctly', async () => {
|
||||
const plaintext = 'Hello, VRCX!';
|
||||
const key = 'my-secret-key';
|
||||
|
||||
const ciphertext = await security.encrypt(plaintext, key);
|
||||
expect(typeof ciphertext).toBe('string');
|
||||
expect(ciphertext.length).toBeGreaterThan(0);
|
||||
|
||||
const decrypted = await security.decrypt(ciphertext, key);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
test('different keys produce different ciphertext', async () => {
|
||||
const plaintext = 'secret data';
|
||||
const ct1 = await security.encrypt(plaintext, 'key1');
|
||||
const ct2 = await security.encrypt(plaintext, 'key2');
|
||||
expect(ct1).not.toBe(ct2);
|
||||
});
|
||||
|
||||
test('decrypt returns empty string for empty input', async () => {
|
||||
const result = await security.decrypt('', 'key');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import * as utils from '../shared/utils';
|
||||
|
||||
const AppDebug = reactive({
|
||||
debug: false,
|
||||
debugWebSocket: false,
|
||||
debugUserDiff: false,
|
||||
debugPhotonLogging: false,
|
||||
debugGameLog: false,
|
||||
debugWebRequests: false,
|
||||
debugFriendState: false,
|
||||
debugIPC: false,
|
||||
debugVrcPlus: false,
|
||||
errorNoty: null,
|
||||
dontLogMeOut: false,
|
||||
endpointDomain: 'https://api.vrchat.cloud/api/1',
|
||||
endpointDomainVrchat: 'https://api.vrchat.cloud/api/1',
|
||||
websocketDomain: 'wss://pipeline.vrchat.cloud',
|
||||
websocketDomainVrchat: 'wss://pipeline.vrchat.cloud'
|
||||
});
|
||||
|
||||
window.$debug = AppDebug;
|
||||
window.utils = utils;
|
||||
window.dayjs = dayjs;
|
||||
|
||||
export { AppDebug };
|
||||
@@ -0,0 +1,169 @@
|
||||
import sqliteService from './sqlite.js';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
function transformKey(key) {
|
||||
return `config:${String(key).toLowerCase()}`;
|
||||
}
|
||||
|
||||
class ConfigRepository {
|
||||
async init() {
|
||||
await sqliteService.executeNonQuery(
|
||||
'CREATE TABLE IF NOT EXISTS configs (`key` TEXT PRIMARY KEY, `value` TEXT)'
|
||||
);
|
||||
}
|
||||
|
||||
async remove(key) {
|
||||
const _key = transformKey(key);
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM configs WHERE key = @key`,
|
||||
{
|
||||
'@key': _key
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} defaultValue
|
||||
* @returns {Promise<string | null>}
|
||||
*/
|
||||
async getString(key, defaultValue = null) {
|
||||
const _key = transformKey(key);
|
||||
let value = undefined;
|
||||
await sqliteService.execute(
|
||||
(row) => {
|
||||
value = row[0];
|
||||
},
|
||||
`SELECT value FROM configs WHERE key = @key`,
|
||||
{
|
||||
'@key': _key
|
||||
}
|
||||
);
|
||||
|
||||
if (value === null || value === undefined || value === 'undefined') {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setString(key, value) {
|
||||
const _key = transformKey(key);
|
||||
const _value = String(value);
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO configs (key, value) VALUES (@key, @value)`,
|
||||
{
|
||||
'@key': _key,
|
||||
'@value': _value
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {boolean} defaultValue
|
||||
* @returns {Promise<boolean | null>}
|
||||
*/
|
||||
async getBool(key, defaultValue = null) {
|
||||
const value = await this.getString(key, null);
|
||||
if (value === null || value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {boolean} value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setBool(key, value) {
|
||||
await this.setString(key, value ? 'true' : 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {number} defaultValue
|
||||
* @returns {Promise<number | null>}
|
||||
*/
|
||||
async getInt(key, defaultValue = null) {
|
||||
let value = await this.getString(key, null);
|
||||
if (value === null || value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
value = parseInt(value, 10);
|
||||
if (isNaN(value) === true) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async setInt(key, value) {
|
||||
await this.setString(key, value);
|
||||
}
|
||||
|
||||
async getFloat(key, defaultValue = null) {
|
||||
let value = await this.getString(key, null);
|
||||
if (value === null || value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
value = parseFloat(value);
|
||||
if (isNaN(value) === true) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async setFloat(key, value) {
|
||||
await this.setString(key, value);
|
||||
}
|
||||
|
||||
async getObject(key, defaultValue = null) {
|
||||
let value = await this.getString(key, null);
|
||||
if (value === null || value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
if (value !== Object(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async setObject(key, value) {
|
||||
await this.setString(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Array} defaultValue
|
||||
* @returns {Promise<Array | null>}
|
||||
*/
|
||||
async getArray(key, defaultValue = null) {
|
||||
const value = await this.getObject(key, null);
|
||||
if (Array.isArray(value) === false) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async setArray(key, value) {
|
||||
await this.setObject(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
var self = new ConfigRepository();
|
||||
window.configRepository = self;
|
||||
|
||||
export { self as default, ConfigRepository, transformKey };
|
||||
@@ -0,0 +1,190 @@
|
||||
/// Copyright © `2019` `https://github.com/gc/` (MIT License)
|
||||
/// This file doesn't support non latin languages very well, but there's not
|
||||
/// much that can be done about that
|
||||
|
||||
const charToConfusables = new Map([
|
||||
[' ', ' '],
|
||||
['0', '⓿'],
|
||||
['1', '11⓵➊⑴¹𝟏𝟙1𝟷𝟣⒈𝟭1➀₁①❶⥠'],
|
||||
['2', '⓶⒉⑵➋ƻ²ᒿ𝟚2𝟮𝟤ᒾ𝟸Ƨ𝟐②ᴤ₂➁❷ᘝƨ'],
|
||||
['3', '³ȝჳⳌꞫ𝟑ℨ𝟛𝟯𝟥Ꝫ➌ЗȜ⓷ӠƷ3𝟹⑶⒊ʒʓǯǮƺ𝕴ᶾзᦡ➂③₃ᶚᴣᴟ❸ҘҙӬӡӭӟӞ'],
|
||||
['4', '𝟰𝟺𝟦𝟒➍ҶᏎ𝟜ҷ⓸ҸҹӴӵᶣ4чㄩ⁴➃₄④❹Ӌ⑷⒋'],
|
||||
['5', '𝟱⓹➎Ƽ𝟓𝟻𝟝𝟧5➄₅⑤⁵❺ƽ⑸⒌'],
|
||||
['6', 'Ⳓ🄇𝟼Ꮾ𝟲𝟞𝟨𝟔➏⓺Ϭϭ⁶б6ᧈ⑥➅₆❻⑹⒍'],
|
||||
['7', '𝟕𝟟𝟩𝟳𝟽🄈⓻𐓒➐7⁷⑦₇❼➆⑺⒎'],
|
||||
['8', '𐌚🄉➑⓼8𝟠𝟪৪⁸₈𝟴➇⑧❽𝟾𝟖⑻⒏'],
|
||||
['9', '൭Ꝯ𝝑𝞋𝟅🄊𝟡𝟵Ⳋ⓽➒੧৭୨9𝟫𝟿𝟗⁹₉Գ➈⑨❾⑼⒐'],
|
||||
['10', '⓾❿➉➓🔟⑩⑽⒑'],
|
||||
['11', '⑪⑾⒒⓫'],
|
||||
['12', '⑫⑿⒓⓬'],
|
||||
['13', '⑬⒀⒔⓭'],
|
||||
['14', '⑭⒁⒕⓮'],
|
||||
['15', '⑮⒂⒖⓯'],
|
||||
['16', '⑯⒃⒗⓰'],
|
||||
['17', '⑰⒄⒘⓱'],
|
||||
['18', '⑱⒅⒙⓲'],
|
||||
['19', '⑲⒆⒚⓳'],
|
||||
['20', '⑳⒇⒛⓴'],
|
||||
['ae', 'æ'],
|
||||
['OE', 'Œ'],
|
||||
['oe', 'œ'],
|
||||
['pi', 'ᒆ'],
|
||||
['Nj', 'Nj'],
|
||||
['AE', 'ᴁ'],
|
||||
[
|
||||
'A',
|
||||
'𝑨𝔄ᗄ𝖠𝗔ꓯ𝞐🄐🄰Ꭿ𐊠𝕬𝜜𝐴ꓮᎪ𝚨ꭺ𝝖🅐Å∀🇦₳🅰𝒜𝘈𝐀𝔸дǺᗅⒶAΑᾋᗩĂÃÅǍȀȂĀȺĄʌΛλƛᴀᴬДАልÄₐᕱªǞӒΆẠẢẦẨẬẮẰẲẴẶᾸᾹᾺΆᾼᾈᾉᾊᾌᾍᾎᾏἈἉἊἋἌἍἎἏḀȦǠӐÀÁÂẤẪ𝛢𝓐𝙰𝘼ᗩ'
|
||||
],
|
||||
[
|
||||
'a',
|
||||
'∂⍺ⓐձǟᵃᶏ⒜аɒaαȃȁคǎმäɑāɐąᾄẚạảǡầẵḁȧӑӓãåάὰάăẩằẳặᾀᾁᾂᾃᾅᾆᾰᾱᾲᾳᾴᶐᾶᾷἀἁἂἃἄἅἆἇᾇậắàáâấẫǻⱥ𝐚𝑎𝒂𝒶𝓪𝔞𝕒𝖆𝖺𝗮𝘢𝙖𝚊𝛂𝛼𝜶𝝰𝞪⍶'
|
||||
],
|
||||
[
|
||||
'B',
|
||||
'🄑𝔙𝖁ꞵ𝛃𝛽𝜷𝝱𝞫Ᏸ𐌁𝑩𝕭🄱𐊡𝖡𝘽ꓐ𝗕𝘉𝜝𐊂𝚩𝐁𝛣𝝗𝐵𝙱𝔹Ᏼᏼ𝞑Ꞵ𝔅🅑฿𝓑ᗿᗾᗽ🅱ⒷBвϐᗷƁ乃ßცჩ๖βɮБՅ๒ᙖʙᴮᵇጌḄℬΒВẞḂḆɃദᗹᗸᵝᙞᙟᙝᛒᙗᙘᴃ🇧'
|
||||
],
|
||||
['b', 'ꮟᏏ𝐛𝘣𝒷𝔟𝓫𝖇𝖻𝑏𝙗𝕓𝒃𝗯𝚋♭ᑳᒈbᖚᕹᕺⓑḃḅҍъḇƃɓƅᖯƄЬᑲþƂ⒝ЪᶀᑿᒀᒂᒁᑾьƀҌѢѣᔎ'],
|
||||
[
|
||||
'C',
|
||||
'ꞆႠ℃🄒ᏟⲤ🄲ꓚ𐊢𐌂🅲𐐕🅒☾ČÇⒸCↃƇᑕㄈ¢८↻ĈϾՇȻᙅᶜ⒞ĆҀĊ©टƆℂℭϹС匚ḈҪʗᑖᑡᑢᑣᑤᑥⅭ𝐂𝐶𝑪𝒞𝓒𝕮𝖢𝗖𝘊𝘾ᔍ'
|
||||
],
|
||||
[
|
||||
'c',
|
||||
'🝌cⅽ𝐜𝑐𝒄𝒸𝓬𝔠𝕔𝖈𝖼𝗰𝘤𝙘𝚌ᴄϲⲥсꮯ𐐽ⲥ𐐽ꮯĉcⓒćčċçҁƈḉȼↄсርᴄϲҫ꒝ςɽϛ𝙲ᑦ᧚𝐜𝑐𝒄𝒸𝓬𝔠𝕔𝖈𝖼𝗰𝘤𝙘𝚌₵🇨ᥴᒼⅽ'
|
||||
],
|
||||
['D', '🄓Ꭰ🄳𝔡𝖉𝔻𝗗𝘋𝙳𝐷𝓓𝐃𝑫𝕯𝖣𝔇𝘿ꭰⅅ𝒟ꓓ🅳🅓ⒹDƉᗪƊÐԺᴅᴰↁḊĐÞⅮᗞᑯĎḌḐḒḎᗫᗬᗟᗠᶛᴆ🇩'],
|
||||
['d', 'Ꮷ𝔡𝖉ᑯꓒ𝓭ᵭ₫ԃⓓdḋďḍḑḓḏđƌɖɗᵈ⒟ԁⅾᶁԀᑺᑻᑼᑽᒄᑰᑱᶑ𝕕𝖽𝑑𝘥𝒅𝙙𝐝𝗱𝚍ⅆ𝒹ʠժ'],
|
||||
[
|
||||
'E',
|
||||
'£ᙓ⋿∃ⴺꓱ𝐄𝐸𝔈𝕰𝖤𝘌𝙴𝛦𝜠ꭼ🄔🄴𝙀𝔼𐊆𝚬ꓰ𝝚𝞔𝓔𝑬𝗘🅴🅔ⒺΈEƎἝᕮƐモЄᴇᴱᵉÉ乇ЁɆꂅ€ÈℰΕЕⴹᎬĒĔĖĘĚÊËԐỀẾỄỂẼḔḖẺȄȆẸỆȨḜḘḚἘἙἚἛἜῈΈӖὲέЀϵ🇪'
|
||||
],
|
||||
[
|
||||
'e',
|
||||
'əәⅇꬲꞓ⋴𝛆𝛜𝜀𝜖𝜺𝝐𝝴𝞊𝞮𝟄ⲉꮛ𐐩ꞒⲈ⍷𝑒𝓮𝕖𝖊𝘦𝗲𝚎𝙚𝒆𝔢𝖾𝐞Ҿҿⓔe⒠èᧉéᶒêɘἔềếễ૯ǝєεēҽɛểẽḕḗĕėëẻěȅȇẹệȩɇₑęḝḙḛ℮еԑѐӗᥱёἐἑἒἓἕℯ'
|
||||
],
|
||||
['F', 'ᖵꘘꓞꟻᖷ𝐅𝐹𝑭𝔽𝕱𝖥𝗙𝙁𝙵𝟊℉🄕🄵𐊇𝔉𝘍𐊥ꓝꞘ🅵🅕𝓕ⒻFғҒᖴƑԲϝቻḞℱϜ₣🇫Ⅎ'],
|
||||
['f', '𝐟ᵮ𝑓𝒇𝒻𝓯𝔣𝕗𝖿𝗳𝙛𝚏ꬵꞙẝ𝖋ⓕfƒḟʃբᶠ⒡ſꊰʄ∱ᶂ𝘧'],
|
||||
['G', '𝗚𝘎🄖ꓖᏳ🄶Ꮐᏻ𝔾𝓖𝑮𝕲ꮐ𝒢𝙂𝖦𝙶𝔊𝐺𝐆🅶🅖ⒼGɢƓʛĢᘜᴳǴĠԌĜḠĞǦǤԍ₲🇬⅁'],
|
||||
['g', 'ᶃᶢⓖgǵĝḡğġǧģց૭ǥɠﻭﻮᵍ⒢ℊɡᧁ𝐠𝑔𝒈𝓰𝔤𝕘𝖌𝗀𝗴𝘨𝙜𝚐'],
|
||||
[
|
||||
'H',
|
||||
'Ἤ🄗𝆦🄷𝜢ꓧ𝘏𝐻𝝜𝖧𐋏𝗛ꮋℍᎻℌⲎ𝑯𝞖🅷🅗ዞǶԋⒽHĤᚺḢḦȞḤḨḪĦⱧҢңҤῊΉῌἨἩἪἫἭἮἯᾘᾙᾚᾛᾜᾝᾞᾟӉӈҥΉн卄♓𝓗ℋН𝐇𝙃𝙷ʜ𝛨Η𝚮ᕼӇᴴᵸ🇭'
|
||||
],
|
||||
['h', 'ꞕ৸𝕳ꚕᏲℏӊԊꜧᏂҺ⒣ђⓗhĥḣḧȟḥḩḫẖħⱨհһከኩኪካɦℎ𝐡𝒉𝒽𝓱𝔥𝕙𝖍𝗁𝗵𝘩𝙝𝚑իʰᑋᗁɧんɥ'],
|
||||
[
|
||||
'I',
|
||||
'ⲒἿ🄘🄸ЇꀤᏆ🅸🅘إﺇٳأﺃٲٵⒾI៸ÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗェエῘῙῚΊἸἹἺἻἼἽἾⅠΪΊɪᶦᑊᥣ𝛪𝐈𝙄𝙸𝓵𝙡𝐼ᴵ𝚰𝑰🇮'
|
||||
],
|
||||
[
|
||||
'i',
|
||||
'⍳ℹⅈ𝑖𝒊𝒾ı𝚤ɩιιͺ𝛊𝜄𝜾𝞲ꙇӏꭵᎥⓘiìíîĩīĭïḯỉǐȉȋịḭῐῑῒΐῖῗἰἱἲⅰⅼ∣ⵏ│׀ا١۱ߊᛁἳἴἵɨіὶίᶖ𝔦𝚒𝝸𝗂𝐢𝕚𝖎𝗶𝘪𝙞ίⁱᵢ𝓲⒤'
|
||||
],
|
||||
['J', '𝐉𝐽𝑱𝒥𝓙𝔍𝕁𝕵𝖩𝗝𝘑𝙅𝙹ꞲͿꓙ🄙🄹🅹🅙ⒿJЈʝᒍנフĴʆวلյʖᴊᴶﻝጋɈⱼՂๅႱįᎫȷ丿ℐℑᒘᒙᒚᒛᒴᒵᒎᒏ🇯'],
|
||||
['j', '𝚥ꭻⅉⓙjϳʲ⒥ɉĵǰјڶᶨ𝒿𝘫𝗷𝑗𝙟𝔧𝒋𝗃𝓳𝕛𝚓𝖏𝐣'],
|
||||
['K', '𝐊ꝄꝀ𝐾𝑲𝓚𝕶𝖪𝙺𝚱𝝟🄚𝗞🄺𝜥𝘒ꓗ𝙆𝕂Ⲕ𝔎𝛫Ꮶ𝞙𝒦🅺🅚₭ⓀKĸḰќƘкҠκқҟӄʞҚКҡᴋᴷᵏ⒦ᛕЌጕḲΚKҜҝҞĶḴǨⱩϗӃ🇰'],
|
||||
['k', 'ⓚꝁkḱǩḳķḵƙⱪᶄ𝐤𝘬𝗄𝕜𝜅𝜘𝜿𝝒𝝹𝞌𝞳𝙠𝚔𝑘𝒌ϰ𝛋𝛞𝟆𝗸𝓴𝓀'],
|
||||
[
|
||||
'L',
|
||||
'𝐋𝐿𝔏𝕃𝕷𝖫𝗟𝘓𝙇ﴼ🄛🄻𐐛Ⳑ𝑳𝙻𐑃𝓛ⳑꮮᏞꓡ🅻🅛ﺈ└ⓁւLĿᒪ乚ՆʟꓶιԼᴸˡĹረḶₗΓլĻᄂⅬℒⱢᥧᥨᒻᒶᒷᶫﺎᒺᒹᒸᒫ⎳ㄥŁⱠﺄȽ🇱'
|
||||
],
|
||||
['l', 'ⓛlŀĺľḷḹļӀℓḽḻłレɭƚɫⱡ|Ɩ⒧ʅǀוןΙІ|ᶩӏ𝓘𝕀𝖨𝗜𝘐𝐥𝑙𝒍𝓁𝔩𝕝𝖑𝗅𝗹𝘭𝚕𝜤𝝞ı𝚤ɩι𝛊𝜄𝜾𝞲'],
|
||||
['M', 'ꮇ🄜🄼𐌑𐊰ꓟⲘᎷ🅼🅜ⓂMмṂ൱ᗰ州ᘻო๓♏ʍᙏᴍᴹᵐ⒨ḾМṀ௱ⅯℳΜϺᛖӍӎ𝐌𝑀𝑴𝓜𝔐𝕄𝕸𝖬𝗠𝘔𝙈𝙼𝚳𝛭𝜧𝝡𝞛🇲'],
|
||||
['m', '₥ᵯ𝖒𝐦𝗆𝔪𝕞𝓂ⓜmനᙢ൩ḿṁⅿϻṃጠɱ៳ᶆ𝙢𝓶𝚖𝑚𝗺᧕᧗'],
|
||||
[
|
||||
'N',
|
||||
'𝇙𝇚𝇜🄝𝆧𝙉🄽ℕꓠ𝛮𝝢𝙽𝚴𝑵𝑁Ⲛ𝐍𝒩𝞜𝗡𝘕𝜨𝓝𝖭🅽₦🅝ЙЍⓃҋ៷NᴎɴƝᑎ几иՈռИהЛπᴺᶰŃ刀ክṄⁿÑПΝᴨոϖǸŇṆŅṊṈทŊӢӣӤӥћѝйᥢҊᴻ🇳'
|
||||
],
|
||||
[
|
||||
'n',
|
||||
'ոռח𝒏𝓷𝙣𝑛𝖓𝔫𝗇𝚗𝗻ᥒⓝήnǹᴒńñᾗηṅňṇɲņṋṉղຖՌƞŋ⒩ภกɳпʼnлԉȠἠἡῃդᾐᾑᾒᾓᾔᾕᾖῄῆῇῂἢἣἤἥἦἧὴήበቡቢባቤብቦȵ𝛈𝜂𝜼𝝶𝞰𝕟𝘯𝐧𝓃ᶇᵰᥥ∩'
|
||||
],
|
||||
[
|
||||
'O',
|
||||
'𝜽⭘🔿ꭴ⭕⏺🄁🄀Ꭴ𝚯𝚹𝛩𝛳𝜣𝜭𝝝𝝧𝞗𝞡ⴱᎾᏫ⍬𝞱𝝷𝛉𝟎𝜃θ𝟘𝑂𝑶𝓞𝔒𝕆𝕺𝗢𝘖𝙊𝛰㈇ꄲ🄞🔾🄾𐊒𝟬ꓳⲞ𐐄𐊫𐓂𝞞🅞⍥◯ⵁ⊖0⊝𝝤Ѳϴ𝚶𝜪ѺӦӨӪΌʘ𝐎ǑÒŎÓÔÕȌȎㇿ❍ⓄOὋロ❤૦⊕ØФԾΘƠᴼᵒ⒪ŐÖₒ¤◊Φ〇ΟОՕଠഠ௦סỒỐỖỔṌȬṎŌṐṒȮȰȪỎỜỚỠỞỢỌỘǪǬǾƟⵔ߀៰⍜⎔⎕⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃ὈὉὊὌὍ'
|
||||
],
|
||||
[
|
||||
'o',
|
||||
'ంಂംං૦௦۵ℴ𝑜𝒐𝖔ꬽ𝝄𝛔𝜎𝝈𝞂ჿ𝚘০୦ዐ𝛐𝗈𝞼ဝⲟ𝙤၀𐐬𝔬𐓪𝓸🇴⍤○ϙ🅾𝒪𝖮𝟢𝟶𝙾𝘰𝗼𝕠𝜊𝐨𝝾𝞸ᐤⓞѳ᧐ᥲðoఠᦞՓòөӧóºōôǒȏŏồốȍỗổõσṍȭṏὄṑṓȯȫ๏ᴏőöѻоዐǭȱ০୦٥౦೦൦๐໐οօᴑ०੦ỏơờớỡởợọộǫøǿɵծὀὁόὸόὂὃὅ'
|
||||
],
|
||||
['P', '🄟🄿ꓑ𝚸𝙿𝞠𝙋ꮲⲢ𝒫𝝦𝑃𝑷𝗣𝐏𐊕𝜬𝘗𝓟𝖯𝛲Ꮲ🅟Ҏ🅿ⓅPƤᑭ尸Ṗրφքᴘᴾᵖ⒫ṔアקРየᴩⱣℙΡῬᑸᑶᑷᑹᑬᑮ🇵₱'],
|
||||
['p', 'ⲣҏ℗ⓟpṕṗƥᵽῥρрƿǷῤ⍴𝓹𝓅𝐩𝑝𝒑𝔭𝕡𝖕𝗉𝗽𝘱𝙥𝚙𝛒𝝆𝞺𝜌𝞀'],
|
||||
['Q', '🅀🄠Ꝗ🆀🅠ⓆQℚⵕԚ𝐐𝑄𝑸𝒬𝓠𝚀𝘘𝙌𝖰𝕼𝔔𝗤🇶'],
|
||||
['q', '𝓆ꝗ𝗾ⓠqգ⒬۹զᑫɋɊԛ𝗊𝑞𝘲𝕢𝚚𝒒𝖖𝐪𝔮𝓺𝙦'],
|
||||
['R', '℞🄡℟ꭱᏒ𐒴ꮢᎡꓣ🆁🅡ⓇRᴙȒʀᖇя尺ŔЯરƦᴿዪṚɌʁℛℜℝṘŘȐṜŖṞⱤ𝐑𝑅𝑹𝓡𝕽𝖱𝗥𝘙𝙍𝚁ᚱ🇷ᴚ'],
|
||||
['r', '𝚛ꭇᣴℾ𝚪𝛤𝜞𝝘𝞒ⲄГᎱᒥꭈⲅꮁⓡrŕṙřȑȓṛṝŗгՐɾᥬṟɍʳ⒭ɼѓᴦᶉ𝐫𝑟𝒓𝓇𝓻𝔯𝕣𝖗𝗋𝗿𝘳𝙧ᵲґᵣ'],
|
||||
['S', '🅂🄪🄢ꇙ𝓢𝗦Ꮪ𝒮Ꮥ𝚂𝐒ꓢ𝖲𝔖𝙎𐊖𝕾𐐠𝘚𝕊𝑆𝑺🆂🅢ⓈSṨŞֆՏȘˢ⒮ЅṠŠŚṤŜṦṢടᔕᔖᔢᔡᔣᔤ'],
|
||||
['s', 'ᣵⓢꜱ𐑈ꮪsśṥŝṡšṧʂṣṩѕşșȿᶊక𝐬𝑠𝒔𝓈𝓼𝔰𝕤𝖘𝗌𝘀𝘴𝙨𝚜ގ🇸'],
|
||||
[
|
||||
'T',
|
||||
'🅃🄣七ፒ𝜯🆃𐌕𝚻𝛵𝕋𝕿𝑻𐊱𐊗𝖳𝙏🝨𝝩𝞣𝚃𝘛𝑇ꓔ⟙𝐓Ⲧ𝗧⊤𝔗Ꭲꭲ𝒯🅣⏇⏉ⓉTтҬҭƬイŦԵτᴛᵀイፕϮŤ⊥ƮΤТ下ṪṬȚŢṰṮ丅丁ᐪ𝛕𝜏𝝉𝞃𝞽𝓣ㄒ🇹ጥ'
|
||||
],
|
||||
['t', 'ⓣtṫẗťṭțȶ੮էʇ†ţṱṯƭŧᵗ⒯ʈեƫ𝐭𝑡𝒕𝓉𝓽𝔱𝕥𝖙𝗍𝘁𝘵𝙩𝚝ナ'],
|
||||
[
|
||||
'U',
|
||||
'🅄Џ🄤ሀꓴ𐓎꒤🆄🅤ŨŬŮᑗᑘǓǕǗǙⓊUȖᑌ凵ƱմԱꓵЦŪՄƲᙀᵁᵘ⒰ŰપÜՍÙÚÛṸṺǛỦȔƯỪỨỮỬỰỤṲŲṶṴɄᥩᑧ∪ᘮ⋃𝐔𝑈𝑼𝒰𝓤𝔘𝕌𝖀𝖴𝗨𝘜𝙐𝚄🇺'
|
||||
],
|
||||
[
|
||||
'u',
|
||||
'𝘂𝘶𝙪𝚞ꞟꭎꭒ𝛖𝜐𝝊𝞄𝞾𐓶ὺύⓤuùũūừṷṹŭǖữᥙǚǜὗυΰนսʊǘǔúůᴜűųยûṻцሁüᵾᵤµʋủȕȗưứửựụṳṵʉῠῡῢΰῦῧὐὑϋύὒὓὔὕὖᥔ𝐮𝑢𝒖𝓊𝓾𝔲𝕦𝖚𝗎ᶙ'
|
||||
],
|
||||
['V', '𝑉𝒱𝕍𝗩🄥🅅ꓦ𝑽𝖵𝘝Ꮩ𝚅𝙑𝐕🆅🅥ⓋVᐯѴᵛ⒱۷ṾⅴⅤṼ٧ⴸѶᐺᐻ🇻𝓥'],
|
||||
['v', '∨⌄⋁ⅴ𝐯𝑣𝒗𝓋𝔳𝕧𝖛𝗏ꮩሀⓥv𝜐𝝊ṽṿ౮งѵעᴠνטᵥѷ៴ᘁ𝙫𝚟𝛎𝜈𝝂𝝼𝞶𝘷𝘃𝓿'],
|
||||
['W', '𝐖𝑊𝓦𝔚𝕎𝖂𝖶𝗪𝙒𝚆🄦🅆ᏔᎳ𝑾ꓪ𝒲𝘞🆆Ⓦ🅦wWẂᾧᗯᥕ山ѠຟచաЩШώщฬшᙎᵂʷ⒲ฝሠẄԜẀŴẆẈധᘺѿᙡƜ₩🇼'],
|
||||
['w', '𝐰ꝡ𝑤𝒘𝓌𝔀𝔴𝕨𝖜𝗐𝘄𝘸𝙬𝚠աẁꮃẃⓦ⍵ŵẇẅẘẉⱳὼὠὡὢὣωὤὥὦὧῲῳῴῶῷⱲѡԝᴡώᾠᾡᾢᾣᾤᾥᾦɯ𝝕𝟉𝞏'],
|
||||
[
|
||||
'X',
|
||||
'ꭓꭕ𝛘𝜒𝝌𝞆𝟀ⲭ🞨𝑿𝛸🄧🞩🞪🅇🞫🞬𐌗Ⲭꓫ𝖃𝞦𝘟𐊐𝚾𝝬𝜲Ꭓ𐌢𝖷𝑋𝕏𝔛𐊴𝗫🆇🅧❌Ⓧ𝓧XẊ᙭χㄨ𝒳ӾჯӼҳЖΧҲᵡˣ⒳אሸẌꊼⅩХ╳᙮ᕁᕽⅹᚷⵝ𝙓𝚇乂𝐗🇽'
|
||||
],
|
||||
['x', '᙮ⅹ𝑥𝒙𝓍𝔵𝕩𝖝𝗑𝘅ᕁᕽⓧxхẋ×ₓ⤫⤬⨯ẍᶍ𝙭ӽ𝘹𝐱𝚡⨰メ𝔁'],
|
||||
[
|
||||
'Y',
|
||||
'𝒴🄨𝓨𝔜𝖄𝖸𝘠𝙔𝚼𝛶𝝪𝞤УᎩᎽⲨ𝚈𝑌𝗬𝐘ꓬ𝒀𝜰𐊲🆈🅨ⓎYὛƳㄚʏ⅄ϔ¥¥ՎϓγץӲЧЎሃŸɎϤΥϒҮỲÝŶỸȲẎỶỴῨῩῪΎὙὝὟΫΎӮӰҰұ𝕐🇾'
|
||||
],
|
||||
['y', '𝐲𝑦𝒚𝓎𝔂𝔶𝕪𝖞𝗒𝘆𝘺𝙮𝚢ʏỿꭚγℽ𝛄𝛾𝜸𝝲𝞬🅈ᎽᎩⓨyỳýŷỹȳẏÿỷуყẙỵƴɏᵞɣʸᶌү⒴ӳӱӯўУʎ'],
|
||||
['Z', '🄩🅉ꓜ𝗭𝐙☡Ꮓ𝘡🆉🅩ⓏZẔƵ乙ẐȤᶻ⒵ŹℤΖŻŽẒⱫ🇿'],
|
||||
['z', '𝑍𝒁𝒵𝓩𝖹𝙕𝚉𝚭𝛧𝜡𝝛𝞕ᵶꮓ𝐳𝑧𝒛𝓏𝔃𝔷𝕫𝖟𝗓𝘇𝘻𝙯𝚣ⓩzźẑżžẓẕƶȥɀᴢጊʐⱬᶎʑᙆ']
|
||||
]);
|
||||
|
||||
/** @copyright Mathias Bynens <https://mathiasbynens.be/>. MIT license. */
|
||||
const regexLineBreakCombiningMarks =
|
||||
/[\0-\x08\x0E-\x1F\x7F-\x84\x86-\x9F\u0300-\u034E\u0350-\u035B\u0363-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u061C\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u200C\u200E\u200F\u202A-\u202E\u2066-\u206F\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3035\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFFF9-\uFFFB]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD804[\uDC00-\uDC02\uDC38-\uDC46\uDC7F-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD73\uDD80-\uDD82\uDDB3-\uDDC0\uDDCA-\uDDCC\uDE2C-\uDE37\uDE3E\uDEDF-\uDEEA\uDF00-\uDF03\uDF3C\uDF3E-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF57\uDF62\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC35-\uDC46\uDCB0-\uDCC3\uDDAF-\uDDB5\uDDB8-\uDDC0\uDDDC\uDDDD\uDE30-\uDE40\uDEAB-\uDEB7]|\uD807[\uDC2F-\uDC36\uDC38-\uDC3F\uDC92-\uDCA7\uDCA9-\uDCB6]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF51-\uDF7E\uDF8F-\uDF92]|\uD82F[\uDC9D\uDC9E\uDCA0-\uDCA3]|\uD834[\uDD65-\uDD69\uDD6D-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD4A]|\uDB40[\uDC01\uDC20-\uDC7F\uDD00-\uDDEF]/g;
|
||||
/** @copyright Mathias Bynens <https://mathiasbynens.be/>. MIT license. */
|
||||
const regexSymbolWithCombiningMarks =
|
||||
/([\0-\u02FF\u0370-\u1AAF\u1B00-\u1DBF\u1E00-\u20CF\u2100-\uD7FF\uE000-\uFE1F\uFE30-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])([\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]+)/g;
|
||||
|
||||
const confusablesToChar = new Map();
|
||||
for (const [char_, confusables] of charToConfusables) {
|
||||
for (const confusable of confusables) {
|
||||
confusablesToChar.set(confusable, char_);
|
||||
}
|
||||
}
|
||||
|
||||
const nonConfusables = /^[!-~]*$/;
|
||||
const removeConfusables = function (a) {
|
||||
// Skip if all characters are ok
|
||||
if (nonConfusables.test(a)) {
|
||||
return a;
|
||||
}
|
||||
|
||||
let ret = '';
|
||||
for (const char_ of a
|
||||
.normalize()
|
||||
.replace(regexLineBreakCombiningMarks, '')
|
||||
.replace(regexSymbolWithCombiningMarks, '$1')
|
||||
.replace(/\s/g, '')) {
|
||||
ret += confusablesToChar.get(char_) || char_;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
const removeWhitespace = function (a) {
|
||||
return a.replace(/\s/g, '');
|
||||
};
|
||||
|
||||
export {
|
||||
removeConfusables as default,
|
||||
confusablesToChar,
|
||||
charToConfusables,
|
||||
removeWhitespace
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const avatarFavorites = {
|
||||
addAvatarToCache(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO cache_avatar (id, added_at, author_id, author_name, created_at, description, image_url, name, release_status, thumbnail_image_url, updated_at, version) VALUES (@id, @added_at, @author_id, @author_name, @created_at, @description, @image_url, @name, @release_status, @thumbnail_image_url, @updated_at, @version)`,
|
||||
{
|
||||
'@id': entry.id,
|
||||
'@added_at': new Date().toJSON(),
|
||||
'@author_id': entry.authorId,
|
||||
'@author_name': entry.authorName,
|
||||
'@created_at': entry.created_at,
|
||||
'@description': entry.description,
|
||||
'@image_url': entry.imageUrl,
|
||||
'@name': entry.name,
|
||||
'@release_status': entry.releaseStatus,
|
||||
'@thumbnail_image_url': entry.thumbnailImageUrl,
|
||||
'@updated_at': entry.updated_at,
|
||||
'@version': entry.version
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addAvatarToHistory(avatarId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT INTO ${dbVars.userPrefix}_avatar_history (avatar_id, created_at, time)
|
||||
VALUES (@avatar_id, @created_at, 0)
|
||||
ON CONFLICT(avatar_id) DO UPDATE SET created_at = @created_at`,
|
||||
{
|
||||
'@avatar_id': avatarId,
|
||||
'@created_at': new Date().toJSON()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getAvatarTimeSpent(avatarId) {
|
||||
var ref = {
|
||||
timeSpent: 0,
|
||||
avatarId
|
||||
};
|
||||
await sqliteService.execute(
|
||||
(row) => {
|
||||
ref.timeSpent = row[0];
|
||||
},
|
||||
`SELECT time FROM ${dbVars.userPrefix}_avatar_history WHERE avatar_id = @avatarId`,
|
||||
{
|
||||
'@avatarId': avatarId
|
||||
}
|
||||
);
|
||||
|
||||
return ref;
|
||||
},
|
||||
|
||||
addAvatarTimeSpent(avatarId, timeSpent) {
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE ${dbVars.userPrefix}_avatar_history SET time = time + @timeSpent WHERE avatar_id = @avatarId`,
|
||||
{
|
||||
'@avatarId': avatarId,
|
||||
'@timeSpent': timeSpent
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getAvatarHistory(currentUserId, limit = 100) {
|
||||
var data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
id: dbRow[0],
|
||||
authorId: dbRow[5],
|
||||
authorName: dbRow[6],
|
||||
created_at: dbRow[7],
|
||||
description: dbRow[8],
|
||||
imageUrl: dbRow[9],
|
||||
name: dbRow[10],
|
||||
releaseStatus: dbRow[11],
|
||||
thumbnailImageUrl: dbRow[12],
|
||||
updated_at: dbRow[13],
|
||||
version: dbRow[14]
|
||||
};
|
||||
data.push(row);
|
||||
}, `SELECT * FROM ${dbVars.userPrefix}_avatar_history INNER JOIN cache_avatar ON cache_avatar.id = ${dbVars.userPrefix}_avatar_history.avatar_id WHERE author_id != '${currentUserId}' ORDER BY ${dbVars.userPrefix}_avatar_history.created_at DESC LIMIT ${limit}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
async getCachedAvatarById(id) {
|
||||
var data = null;
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data = {
|
||||
id: dbRow[0],
|
||||
// added_at: dbRow[1],
|
||||
authorId: dbRow[2],
|
||||
authorName: dbRow[3],
|
||||
created_at: dbRow[4],
|
||||
description: dbRow[5],
|
||||
imageUrl: dbRow[6],
|
||||
name: dbRow[7],
|
||||
releaseStatus: dbRow[8],
|
||||
thumbnailImageUrl: dbRow[9],
|
||||
updated_at: dbRow[10],
|
||||
version: dbRow[11]
|
||||
};
|
||||
},
|
||||
`SELECT * FROM cache_avatar WHERE id = @id`,
|
||||
{
|
||||
'@id': id
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
|
||||
clearAvatarHistory() {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_avatar_history`
|
||||
);
|
||||
sqliteService.executeNonQuery('DELETE FROM cache_avatar');
|
||||
},
|
||||
|
||||
addAvatarToFavorites(avatarId, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
'INSERT OR REPLACE INTO favorite_avatar (avatar_id, group_name, created_at) VALUES (@avatar_id, @group_name, @created_at)',
|
||||
{
|
||||
'@avatar_id': avatarId,
|
||||
'@group_name': groupName,
|
||||
'@created_at': new Date().toJSON()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
renameAvatarFavoriteGroup(newGroupName, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE favorite_avatar SET group_name = @new_group_name WHERE group_name = @group_name`,
|
||||
{
|
||||
'@new_group_name': newGroupName,
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteAvatarFavoriteGroup(groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM favorite_avatar WHERE group_name = @group_name`,
|
||||
{
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
removeAvatarFromFavorites(avatarId, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM favorite_avatar WHERE avatar_id = @avatar_id AND group_name = @group_name`,
|
||||
{
|
||||
'@avatar_id': avatarId,
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getAvatarFavorites() {
|
||||
var data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
created_at: dbRow[1],
|
||||
avatarId: dbRow[2],
|
||||
groupName: dbRow[3]
|
||||
};
|
||||
data.push(row);
|
||||
}, 'SELECT * FROM favorite_avatar');
|
||||
return data;
|
||||
},
|
||||
|
||||
removeAvatarFromCache(avatarId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM cache_avatar WHERE id = @avatar_id`,
|
||||
{
|
||||
'@avatar_id': avatarId
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getAvatarCache() {
|
||||
var data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
id: dbRow[0],
|
||||
// added_at: dbRow[1],
|
||||
authorId: dbRow[2],
|
||||
authorName: dbRow[3],
|
||||
created_at: dbRow[4],
|
||||
description: dbRow[5],
|
||||
imageUrl: dbRow[6],
|
||||
name: dbRow[7],
|
||||
releaseStatus: dbRow[8],
|
||||
thumbnailImageUrl: dbRow[9],
|
||||
updated_at: dbRow[10],
|
||||
version: dbRow[11]
|
||||
};
|
||||
data.push(row);
|
||||
}, 'SELECT * FROM cache_avatar');
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
export { avatarFavorites };
|
||||
@@ -0,0 +1,82 @@
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const avatarTags = {
|
||||
async getAvatarTags(avatarId) {
|
||||
const tags = [];
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
tags.push({ tag: dbRow[0], color: dbRow[1] || null });
|
||||
},
|
||||
`SELECT tag, color FROM avatar_tags WHERE avatar_id = @avatar_id`,
|
||||
{
|
||||
'@avatar_id': avatarId
|
||||
}
|
||||
);
|
||||
return tags;
|
||||
},
|
||||
|
||||
async getAllAvatarTags() {
|
||||
const map = new Map();
|
||||
await sqliteService.execute((dbRow) => {
|
||||
const avatarId = dbRow[0];
|
||||
const tag = dbRow[1];
|
||||
const color = dbRow[2] || null;
|
||||
if (!map.has(avatarId)) {
|
||||
map.set(avatarId, []);
|
||||
}
|
||||
map.get(avatarId).push({ tag, color });
|
||||
}, `SELECT avatar_id, tag, color FROM avatar_tags`);
|
||||
return map;
|
||||
},
|
||||
|
||||
async getAllDistinctTags() {
|
||||
const tags = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tags.push(dbRow[0]);
|
||||
}, `SELECT DISTINCT tag FROM avatar_tags ORDER BY tag`);
|
||||
return tags;
|
||||
},
|
||||
|
||||
async addAvatarTag(avatarId, tag, color = null) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO avatar_tags (avatar_id, tag, color) VALUES (@avatar_id, @tag, @color)`,
|
||||
{
|
||||
'@avatar_id': avatarId,
|
||||
'@tag': tag,
|
||||
'@color': color
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async updateAvatarTagColor(avatarId, tag, color) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`UPDATE avatar_tags SET color = @color WHERE avatar_id = @avatar_id AND tag = @tag`,
|
||||
{
|
||||
'@avatar_id': avatarId,
|
||||
'@tag': tag,
|
||||
'@color': color
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async removeAvatarTag(avatarId, tag) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM avatar_tags WHERE avatar_id = @avatar_id AND tag = @tag`,
|
||||
{
|
||||
'@avatar_id': avatarId,
|
||||
'@tag': tag
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async removeAllAvatarTags(avatarId) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM avatar_tags WHERE avatar_id = @avatar_id`,
|
||||
{
|
||||
'@avatar_id': avatarId
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { avatarTags };
|
||||
@@ -0,0 +1,578 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const feed = {
|
||||
addGPSToDatabase(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_feed_gps (created_at, user_id, display_name, location, world_name, previous_location, time, group_name) VALUES (@created_at, @user_id, @display_name, @location, @world_name, @previous_location, @time, @group_name)`,
|
||||
{
|
||||
'@created_at': entry.created_at,
|
||||
'@user_id': entry.userId,
|
||||
'@display_name': entry.displayName,
|
||||
'@location': entry.location,
|
||||
'@world_name': entry.worldName,
|
||||
'@previous_location': entry.previousLocation,
|
||||
'@time': entry.time,
|
||||
'@group_name': entry.groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addStatusToDatabase(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_feed_status (created_at, user_id, display_name, status, status_description, previous_status, previous_status_description) VALUES (@created_at, @user_id, @display_name, @status, @status_description, @previous_status, @previous_status_description)`,
|
||||
{
|
||||
'@created_at': entry.created_at,
|
||||
'@user_id': entry.userId,
|
||||
'@display_name': entry.displayName,
|
||||
'@status': entry.status,
|
||||
'@status_description': entry.statusDescription,
|
||||
'@previous_status': entry.previousStatus,
|
||||
'@previous_status_description': entry.previousStatusDescription
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addBioToDatabase(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_feed_bio (created_at, user_id, display_name, bio, previous_bio) VALUES (@created_at, @user_id, @display_name, @bio, @previous_bio)`,
|
||||
{
|
||||
'@created_at': entry.created_at,
|
||||
'@user_id': entry.userId,
|
||||
'@display_name': entry.displayName,
|
||||
'@bio': entry.bio,
|
||||
'@previous_bio': entry.previousBio
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addAvatarToDatabase(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_feed_avatar (created_at, user_id, display_name, owner_id, avatar_name, current_avatar_image_url, current_avatar_thumbnail_image_url, previous_current_avatar_image_url, previous_current_avatar_thumbnail_image_url) VALUES (@created_at, @user_id, @display_name, @owner_id, @avatar_name, @current_avatar_image_url, @current_avatar_thumbnail_image_url, @previous_current_avatar_image_url, @previous_current_avatar_thumbnail_image_url)`,
|
||||
{
|
||||
'@created_at': entry.created_at,
|
||||
'@user_id': entry.userId,
|
||||
'@display_name': entry.displayName,
|
||||
'@owner_id': entry.ownerId,
|
||||
'@avatar_name': entry.avatarName,
|
||||
'@current_avatar_image_url': entry.currentAvatarImageUrl,
|
||||
'@current_avatar_thumbnail_image_url':
|
||||
entry.currentAvatarThumbnailImageUrl,
|
||||
'@previous_current_avatar_image_url':
|
||||
entry.previousCurrentAvatarImageUrl,
|
||||
'@previous_current_avatar_thumbnail_image_url':
|
||||
entry.previousCurrentAvatarThumbnailImageUrl
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addOnlineOfflineToDatabase(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_feed_online_offline (created_at, user_id, display_name, type, location, world_name, time, group_name) VALUES (@created_at, @user_id, @display_name, @type, @location, @world_name, @time, @group_name)`,
|
||||
{
|
||||
'@created_at': entry.created_at,
|
||||
'@user_id': entry.userId,
|
||||
'@display_name': entry.displayName,
|
||||
'@type': entry.type,
|
||||
'@location': entry.location,
|
||||
'@world_name': entry.worldName,
|
||||
'@time': entry.time,
|
||||
'@group_name': entry.groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async searchFeedDatabase(
|
||||
search,
|
||||
filters,
|
||||
vipList,
|
||||
maxEntries = dbVars.searchTableSize,
|
||||
dateFrom = '',
|
||||
dateTo = ''
|
||||
) {
|
||||
if (search.startsWith('wrld_') || search.startsWith('grp_')) {
|
||||
return this.getFeedByInstanceId(search, filters, vipList);
|
||||
}
|
||||
let vipQuery = '';
|
||||
const vipArgs = {};
|
||||
if (vipList.length > 0) {
|
||||
const vipPlaceholders = [];
|
||||
vipList.forEach((vip, i) => {
|
||||
const key = `@vip_${i}`;
|
||||
vipArgs[key] = vip;
|
||||
vipPlaceholders.push(key);
|
||||
});
|
||||
vipQuery = `AND user_id IN (${vipPlaceholders.join(', ')})`;
|
||||
}
|
||||
let dateQuery = '';
|
||||
if (dateFrom) {
|
||||
dateQuery += 'AND created_at >= @dateFrom ';
|
||||
}
|
||||
if (dateTo) {
|
||||
dateQuery += 'AND created_at <= @dateTo ';
|
||||
}
|
||||
let gps = true;
|
||||
let status = true;
|
||||
let bio = true;
|
||||
let avatar = true;
|
||||
let online = true;
|
||||
let offline = true;
|
||||
const aviPublic = search.includes('public');
|
||||
const aviPrivate = search.includes('private');
|
||||
if (filters.length > 0) {
|
||||
gps = false;
|
||||
status = false;
|
||||
bio = false;
|
||||
avatar = false;
|
||||
online = false;
|
||||
offline = false;
|
||||
filters.forEach((filter) => {
|
||||
switch (filter) {
|
||||
case 'GPS':
|
||||
gps = true;
|
||||
break;
|
||||
case 'Status':
|
||||
status = true;
|
||||
break;
|
||||
case 'Bio':
|
||||
bio = true;
|
||||
break;
|
||||
case 'Avatar':
|
||||
avatar = true;
|
||||
break;
|
||||
case 'Online':
|
||||
online = true;
|
||||
break;
|
||||
case 'Offline':
|
||||
offline = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
const searchLike = `%${search}%`;
|
||||
const selects = [];
|
||||
const baseColumns = [
|
||||
'id',
|
||||
'created_at',
|
||||
'user_id',
|
||||
'display_name',
|
||||
'type',
|
||||
'location',
|
||||
'world_name',
|
||||
'previous_location',
|
||||
'time',
|
||||
'group_name',
|
||||
'status',
|
||||
'status_description',
|
||||
'previous_status',
|
||||
'previous_status_description',
|
||||
'bio',
|
||||
'previous_bio',
|
||||
'owner_id',
|
||||
'avatar_name',
|
||||
'current_avatar_image_url',
|
||||
'current_avatar_thumbnail_image_url',
|
||||
'previous_current_avatar_image_url',
|
||||
'previous_current_avatar_thumbnail_image_url'
|
||||
].join(', ');
|
||||
if (gps) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'GPS' AS type, location, world_name, previous_location, time, group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_gps WHERE (display_name LIKE @searchLike OR world_name LIKE @searchLike OR group_name LIKE @searchLike) ${dateQuery} ${vipQuery} ORDER BY created_at DESC, id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (status) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'Status' AS type, NULL AS location, NULL AS world_name, NULL AS previous_location, NULL AS time, NULL AS group_name, status, status_description, previous_status, previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_status WHERE (display_name LIKE @searchLike OR status LIKE @searchLike OR status_description LIKE @searchLike) ${dateQuery} ${vipQuery} ORDER BY created_at DESC, id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (bio) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'Bio' AS type, NULL AS location, NULL AS world_name, NULL AS previous_location, NULL AS time, NULL AS group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, bio, previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_bio WHERE (display_name LIKE @searchLike OR bio LIKE @searchLike) ${dateQuery} ${vipQuery} ORDER BY created_at DESC, id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (avatar) {
|
||||
let avatarQuery = '';
|
||||
if (aviPrivate) {
|
||||
avatarQuery = 'OR user_id = owner_id';
|
||||
} else if (aviPublic) {
|
||||
avatarQuery = 'OR user_id != owner_id';
|
||||
}
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'Avatar' AS type, NULL AS location, NULL AS world_name, NULL AS previous_location, NULL AS time, NULL AS group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, owner_id, avatar_name, current_avatar_image_url, current_avatar_thumbnail_image_url, previous_current_avatar_image_url, previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_avatar WHERE (display_name LIKE @searchLike OR avatar_name LIKE @searchLike) ${avatarQuery} ${dateQuery} ${vipQuery} ORDER BY created_at DESC, id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (online || offline) {
|
||||
let query = '';
|
||||
if (!online || !offline) {
|
||||
if (online) {
|
||||
query = "AND type = 'Online'";
|
||||
} else if (offline) {
|
||||
query = "AND type = 'Offline'";
|
||||
}
|
||||
}
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, type, location, world_name, NULL AS previous_location, time, group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_online_offline WHERE (display_name LIKE @searchLike OR world_name LIKE @searchLike OR group_name LIKE @searchLike) ${query} ${dateQuery} ${vipQuery} ORDER BY created_at DESC, id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (selects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const feedDatabase = [];
|
||||
const args = {
|
||||
'@searchLike': searchLike,
|
||||
'@limit': maxEntries,
|
||||
'@perTable': maxEntries,
|
||||
...vipArgs
|
||||
};
|
||||
if (dateFrom) {
|
||||
args['@dateFrom'] = dateFrom;
|
||||
}
|
||||
if (dateTo) {
|
||||
args['@dateTo'] = dateTo;
|
||||
}
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
const type = dbRow[4];
|
||||
const row = {
|
||||
rowId: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
userId: dbRow[2],
|
||||
displayName: dbRow[3],
|
||||
type
|
||||
};
|
||||
switch (type) {
|
||||
case 'GPS':
|
||||
row.location = dbRow[5];
|
||||
row.worldName = dbRow[6];
|
||||
row.previousLocation = dbRow[7];
|
||||
row.time = dbRow[8];
|
||||
row.groupName = dbRow[9];
|
||||
break;
|
||||
case 'Status':
|
||||
row.status = dbRow[10];
|
||||
row.statusDescription = dbRow[11];
|
||||
row.previousStatus = dbRow[12];
|
||||
row.previousStatusDescription = dbRow[13];
|
||||
break;
|
||||
case 'Bio':
|
||||
row.bio = dbRow[14];
|
||||
row.previousBio = dbRow[15];
|
||||
break;
|
||||
case 'Avatar':
|
||||
row.ownerId = dbRow[16];
|
||||
row.avatarName = dbRow[17];
|
||||
row.currentAvatarImageUrl = dbRow[18];
|
||||
row.currentAvatarThumbnailImageUrl = dbRow[19];
|
||||
row.previousCurrentAvatarImageUrl = dbRow[20];
|
||||
row.previousCurrentAvatarThumbnailImageUrl = dbRow[21];
|
||||
break;
|
||||
case 'Online':
|
||||
case 'Offline':
|
||||
row.location = dbRow[5];
|
||||
row.worldName = dbRow[6];
|
||||
row.time = dbRow[8];
|
||||
row.groupName = dbRow[9];
|
||||
break;
|
||||
}
|
||||
feedDatabase.push(row);
|
||||
},
|
||||
`SELECT ${baseColumns} FROM (${selects.join(' UNION ALL ')}) ORDER BY created_at DESC, id DESC LIMIT @limit`,
|
||||
args
|
||||
);
|
||||
return feedDatabase;
|
||||
},
|
||||
|
||||
async lookupFeedDatabase(
|
||||
filters,
|
||||
vipList,
|
||||
maxEntries = dbVars.maxTableSize
|
||||
) {
|
||||
let vipQuery = '';
|
||||
const vipArgs = {};
|
||||
if (vipList.length > 0) {
|
||||
const vipPlaceholders = [];
|
||||
vipList.forEach((vip, i) => {
|
||||
const key = `@vip_${i}`;
|
||||
vipArgs[key] = vip;
|
||||
vipPlaceholders.push(key);
|
||||
});
|
||||
vipQuery = `AND user_id IN (${vipPlaceholders.join(', ')})`;
|
||||
}
|
||||
let gps = true;
|
||||
let status = true;
|
||||
let bio = true;
|
||||
let avatar = true;
|
||||
let online = true;
|
||||
let offline = true;
|
||||
if (filters.length > 0) {
|
||||
gps = false;
|
||||
status = false;
|
||||
bio = false;
|
||||
avatar = false;
|
||||
online = false;
|
||||
offline = false;
|
||||
filters.forEach((filter) => {
|
||||
switch (filter) {
|
||||
case 'GPS':
|
||||
gps = true;
|
||||
break;
|
||||
case 'Status':
|
||||
status = true;
|
||||
break;
|
||||
case 'Bio':
|
||||
bio = true;
|
||||
break;
|
||||
case 'Avatar':
|
||||
avatar = true;
|
||||
break;
|
||||
case 'Online':
|
||||
online = true;
|
||||
break;
|
||||
case 'Offline':
|
||||
offline = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
const selects = [];
|
||||
const baseColumns = [
|
||||
'id',
|
||||
'created_at',
|
||||
'user_id',
|
||||
'display_name',
|
||||
'type',
|
||||
'location',
|
||||
'world_name',
|
||||
'previous_location',
|
||||
'time',
|
||||
'group_name',
|
||||
'status',
|
||||
'status_description',
|
||||
'previous_status',
|
||||
'previous_status_description',
|
||||
'bio',
|
||||
'previous_bio',
|
||||
'owner_id',
|
||||
'avatar_name',
|
||||
'current_avatar_image_url',
|
||||
'current_avatar_thumbnail_image_url',
|
||||
'previous_current_avatar_image_url',
|
||||
'previous_current_avatar_thumbnail_image_url'
|
||||
].join(', ');
|
||||
if (gps) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'GPS' AS type, location, world_name, previous_location, time, group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_gps WHERE 1=1 ${vipQuery} ORDER BY id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (status) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'Status' AS type, NULL AS location, NULL AS world_name, NULL AS previous_location, NULL AS time, NULL AS group_name, status, status_description, previous_status, previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_status WHERE 1=1 ${vipQuery} ORDER BY id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (bio) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'Bio' AS type, NULL AS location, NULL AS world_name, NULL AS previous_location, NULL AS time, NULL AS group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, bio, previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_bio WHERE 1=1 ${vipQuery} ORDER BY id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (avatar) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'Avatar' AS type, NULL AS location, NULL AS world_name, NULL AS previous_location, NULL AS time, NULL AS group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, owner_id, avatar_name, current_avatar_image_url, current_avatar_thumbnail_image_url, previous_current_avatar_image_url, previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_avatar WHERE 1=1 ${vipQuery} ORDER BY id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (online || offline) {
|
||||
let query = '';
|
||||
if (!online || !offline) {
|
||||
if (online) {
|
||||
query = "AND type = 'Online'";
|
||||
} else if (offline) {
|
||||
query = "AND type = 'Offline'";
|
||||
}
|
||||
}
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, type, location, world_name, NULL AS previous_location, time, group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_online_offline WHERE 1=1 ${query} ${vipQuery} ORDER BY id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (selects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const feedDatabase = [];
|
||||
const args = {
|
||||
'@limit': maxEntries,
|
||||
'@perTable': maxEntries,
|
||||
...vipArgs
|
||||
};
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
const type = dbRow[4];
|
||||
const row = {
|
||||
rowId: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
userId: dbRow[2],
|
||||
displayName: dbRow[3],
|
||||
type
|
||||
};
|
||||
switch (type) {
|
||||
case 'GPS':
|
||||
row.location = dbRow[5];
|
||||
row.worldName = dbRow[6];
|
||||
row.previousLocation = dbRow[7];
|
||||
row.time = dbRow[8];
|
||||
row.groupName = dbRow[9];
|
||||
break;
|
||||
case 'Status':
|
||||
row.status = dbRow[10];
|
||||
row.statusDescription = dbRow[11];
|
||||
row.previousStatus = dbRow[12];
|
||||
row.previousStatusDescription = dbRow[13];
|
||||
break;
|
||||
case 'Bio':
|
||||
row.bio = dbRow[14];
|
||||
row.previousBio = dbRow[15];
|
||||
break;
|
||||
case 'Avatar':
|
||||
row.ownerId = dbRow[16];
|
||||
row.avatarName = dbRow[17];
|
||||
row.currentAvatarImageUrl = dbRow[18];
|
||||
row.currentAvatarThumbnailImageUrl = dbRow[19];
|
||||
row.previousCurrentAvatarImageUrl = dbRow[20];
|
||||
row.previousCurrentAvatarThumbnailImageUrl = dbRow[21];
|
||||
break;
|
||||
case 'Online':
|
||||
case 'Offline':
|
||||
row.location = dbRow[5];
|
||||
row.worldName = dbRow[6];
|
||||
row.time = dbRow[8];
|
||||
row.groupName = dbRow[9];
|
||||
break;
|
||||
}
|
||||
feedDatabase.push(row);
|
||||
},
|
||||
`SELECT ${baseColumns} FROM (${selects.join(' UNION ALL ')}) ORDER BY created_at DESC, id DESC LIMIT @limit`,
|
||||
args
|
||||
);
|
||||
return feedDatabase;
|
||||
},
|
||||
|
||||
async getFeedByInstanceId(instanceId, filters, vipList) {
|
||||
let vipQuery = '';
|
||||
const vipArgs = {};
|
||||
if (vipList.length > 0) {
|
||||
const vipPlaceholders = [];
|
||||
vipList.forEach((vip, i) => {
|
||||
const key = `@vip_${i}`;
|
||||
vipArgs[key] = vip;
|
||||
vipPlaceholders.push(key);
|
||||
});
|
||||
vipQuery = `AND user_id IN (${vipPlaceholders.join(', ')})`;
|
||||
}
|
||||
let gps = true;
|
||||
let online = true;
|
||||
let offline = true;
|
||||
if (filters.length > 0) {
|
||||
gps = false;
|
||||
online = false;
|
||||
offline = false;
|
||||
filters.forEach((filter) => {
|
||||
switch (filter) {
|
||||
case 'GPS':
|
||||
gps = true;
|
||||
break;
|
||||
case 'Online':
|
||||
online = true;
|
||||
break;
|
||||
case 'Offline':
|
||||
offline = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
const selects = [];
|
||||
const baseColumns = [
|
||||
'id',
|
||||
'created_at',
|
||||
'user_id',
|
||||
'display_name',
|
||||
'type',
|
||||
'location',
|
||||
'world_name',
|
||||
'previous_location',
|
||||
'time',
|
||||
'group_name',
|
||||
'status',
|
||||
'status_description',
|
||||
'previous_status',
|
||||
'previous_status_description',
|
||||
'bio',
|
||||
'previous_bio',
|
||||
'owner_id',
|
||||
'avatar_name',
|
||||
'current_avatar_image_url',
|
||||
'current_avatar_thumbnail_image_url',
|
||||
'previous_current_avatar_image_url',
|
||||
'previous_current_avatar_thumbnail_image_url'
|
||||
].join(', ');
|
||||
if (gps) {
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, 'GPS' AS type, location, world_name, previous_location, time, group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_gps WHERE location LIKE @instanceLike ${vipQuery} ORDER BY created_at DESC, id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (online || offline) {
|
||||
let query = '';
|
||||
if (!online || !offline) {
|
||||
if (online) {
|
||||
query = "AND type = 'Online'";
|
||||
} else if (offline) {
|
||||
query = "AND type = 'Offline'";
|
||||
}
|
||||
}
|
||||
selects.push(
|
||||
`SELECT * FROM (SELECT id, created_at, user_id, display_name, type, location, world_name, NULL AS previous_location, time, group_name, NULL AS status, NULL AS status_description, NULL AS previous_status, NULL AS previous_status_description, NULL AS bio, NULL AS previous_bio, NULL AS owner_id, NULL AS avatar_name, NULL AS current_avatar_image_url, NULL AS current_avatar_thumbnail_image_url, NULL AS previous_current_avatar_image_url, NULL AS previous_current_avatar_thumbnail_image_url FROM ${dbVars.userPrefix}_feed_online_offline WHERE location LIKE @instanceLike ${query} ${vipQuery} ORDER BY created_at DESC, id DESC LIMIT @perTable)`
|
||||
);
|
||||
}
|
||||
if (selects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const feedDatabase = [];
|
||||
const args = {
|
||||
'@instanceLike': `%${instanceId}%`,
|
||||
'@limit': dbVars.searchTableSize,
|
||||
'@perTable': dbVars.searchTableSize,
|
||||
...vipArgs
|
||||
};
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
const type = dbRow[4];
|
||||
const row = {
|
||||
rowId: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
userId: dbRow[2],
|
||||
displayName: dbRow[3],
|
||||
type
|
||||
};
|
||||
switch (type) {
|
||||
case 'GPS':
|
||||
row.location = dbRow[5];
|
||||
row.worldName = dbRow[6];
|
||||
row.previousLocation = dbRow[7];
|
||||
row.time = dbRow[8];
|
||||
row.groupName = dbRow[9];
|
||||
break;
|
||||
case 'Online':
|
||||
case 'Offline':
|
||||
row.location = dbRow[5];
|
||||
row.worldName = dbRow[6];
|
||||
row.time = dbRow[8];
|
||||
row.groupName = dbRow[9];
|
||||
break;
|
||||
}
|
||||
feedDatabase.push(row);
|
||||
},
|
||||
`SELECT ${baseColumns} FROM (${selects.join(' UNION ALL ')}) ORDER BY created_at DESC, id DESC LIMIT @limit`,
|
||||
args
|
||||
);
|
||||
return feedDatabase;
|
||||
}
|
||||
};
|
||||
|
||||
export { feed };
|
||||
@@ -0,0 +1,58 @@
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const friendFavorites = {
|
||||
addFriendToLocalFavorites(userId, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
'INSERT OR REPLACE INTO favorite_friend (user_id, group_name, created_at) VALUES (@user_id, @group_name, @created_at)',
|
||||
{
|
||||
'@user_id': userId,
|
||||
'@group_name': groupName,
|
||||
'@created_at': new Date().toJSON()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
removeFriendFromLocalFavorites(userId, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM favorite_friend WHERE user_id = @user_id AND group_name = @group_name`,
|
||||
{
|
||||
'@user_id': userId,
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
renameFriendFavoriteGroup(newGroupName, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE favorite_friend SET group_name = @new_group_name WHERE group_name = @group_name`,
|
||||
{
|
||||
'@new_group_name': newGroupName,
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteFriendFavoriteGroup(groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM favorite_friend WHERE group_name = @group_name`,
|
||||
{
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getFriendFavorites() {
|
||||
const data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
const row = {
|
||||
created_at: dbRow[1],
|
||||
userId: dbRow[2],
|
||||
groupName: dbRow[3]
|
||||
};
|
||||
data.push(row);
|
||||
}, 'SELECT * FROM favorite_friend');
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
export { friendFavorites };
|
||||
@@ -0,0 +1,65 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const friendLogCurrent = {
|
||||
async getFriendLogCurrent() {
|
||||
var friendLogCurrent = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
userId: dbRow[0],
|
||||
displayName: dbRow[1],
|
||||
trustLevel: dbRow[2],
|
||||
friendNumber: dbRow[3]
|
||||
};
|
||||
friendLogCurrent.unshift(row);
|
||||
}, `SELECT * FROM ${dbVars.userPrefix}_friend_log_current`);
|
||||
return friendLogCurrent;
|
||||
},
|
||||
|
||||
setFriendLogCurrent(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_friend_log_current (user_id, display_name, trust_level, friend_number) VALUES (@user_id, @display_name, @trust_level, @friend_number)`,
|
||||
{
|
||||
'@user_id': entry.userId,
|
||||
'@display_name': entry.displayName,
|
||||
'@trust_level': entry.trustLevel,
|
||||
'@friend_number': entry.friendNumber
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
setFriendLogCurrentArray(inputData) {
|
||||
if (inputData.length === 0) {
|
||||
return;
|
||||
}
|
||||
var sqlValues = '';
|
||||
var items = ['userId', 'displayName', 'trustLevel'];
|
||||
for (var line of inputData) {
|
||||
var field = {};
|
||||
for (var item of items) {
|
||||
if (typeof line[item] === 'string') {
|
||||
field[item] = line[item].replace(/'/g, "''");
|
||||
} else {
|
||||
field[item] = '';
|
||||
}
|
||||
}
|
||||
sqlValues += `('${field.userId}', '${field.displayName}', '${field.trustLevel}', ${line.friendNumber}), `;
|
||||
}
|
||||
sqlValues = sqlValues.slice(0, -2);
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_friend_log_current (user_id, display_name, trust_level, friend_number) VALUES ${sqlValues}`
|
||||
);
|
||||
},
|
||||
|
||||
deleteFriendLogCurrent(userId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_friend_log_current WHERE user_id = @user_id`,
|
||||
{
|
||||
'@user_id': userId
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { friendLogCurrent };
|
||||
@@ -0,0 +1,129 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const friendLogHistory = {
|
||||
async getFriendLogHistory() {
|
||||
var friendLogHistory = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
rowId: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
type: dbRow[2],
|
||||
userId: dbRow[3],
|
||||
displayName: dbRow[4],
|
||||
friendNumber: dbRow[8]
|
||||
};
|
||||
if (row.type === 'DisplayName') {
|
||||
row.previousDisplayName = dbRow[5];
|
||||
} else if (row.type === 'TrustLevel') {
|
||||
row.trustLevel = dbRow[6];
|
||||
row.previousTrustLevel = dbRow[7];
|
||||
}
|
||||
friendLogHistory.unshift(row);
|
||||
}, `SELECT * FROM ${dbVars.userPrefix}_friend_log_history`);
|
||||
return friendLogHistory;
|
||||
},
|
||||
|
||||
addFriendLogHistory(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_friend_log_history (created_at, type, user_id, display_name, previous_display_name, trust_level, previous_trust_level, friend_number) VALUES (@created_at, @type, @user_id, @display_name, @previous_display_name, @trust_level, @previous_trust_level, @friend_number)`,
|
||||
{
|
||||
'@created_at': entry.created_at,
|
||||
'@type': entry.type,
|
||||
'@user_id': entry.userId,
|
||||
'@display_name': entry.displayName,
|
||||
'@previous_display_name': entry.previousDisplayName,
|
||||
'@trust_level': entry.trustLevel,
|
||||
'@previous_trust_level': entry.previousTrustLevel,
|
||||
'@friend_number': entry.friendNumber
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addFriendLogHistoryArray(inputData) {
|
||||
if (inputData.length === 0) {
|
||||
return;
|
||||
}
|
||||
var sqlValues = '';
|
||||
var items = [
|
||||
'created_at',
|
||||
'type',
|
||||
'userId',
|
||||
'displayName',
|
||||
'previousDisplayName',
|
||||
'trustLevel',
|
||||
'previousTrustLevel',
|
||||
'friendNumber'
|
||||
];
|
||||
for (var i = 0; i < inputData.length; ++i) {
|
||||
var line = inputData[i];
|
||||
sqlValues += '(';
|
||||
for (var k = 0; k < items.length; ++k) {
|
||||
var item = items[k];
|
||||
var field = '';
|
||||
if (typeof line[item] === 'string') {
|
||||
field = `'${line[item].replace(/'/g, "''")}'`;
|
||||
} else {
|
||||
field = null;
|
||||
}
|
||||
sqlValues += field;
|
||||
if (k < items.length - 1) {
|
||||
sqlValues += ', ';
|
||||
}
|
||||
}
|
||||
sqlValues += ')';
|
||||
if (i < inputData.length - 1) {
|
||||
sqlValues += ', ';
|
||||
}
|
||||
// sqlValues `('${line.created_at}', '${line.type}', '${line.userId}', '${line.displayName}', '${line.previousDisplayName}', '${line.trustLevel}', '${line.previousTrustLevel}'), `
|
||||
}
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_friend_log_history (created_at, type, user_id, display_name, previous_display_name, trust_level, previous_trust_level, friend_number) VALUES ${sqlValues}`
|
||||
);
|
||||
},
|
||||
|
||||
async getFriendLogHistoryForUserId(userId, types) {
|
||||
let friendLogHistory = [];
|
||||
let typeFilter = '';
|
||||
if (types && types.length > 0) {
|
||||
const escapedTypes = types.map((t) => `'${t.replace(/'/g, "''")}'`);
|
||||
typeFilter = ` AND type IN (${escapedTypes.join(', ')})`;
|
||||
}
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
const row = {
|
||||
rowId: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
type: dbRow[2],
|
||||
userId: dbRow[3],
|
||||
displayName: dbRow[4],
|
||||
friendNumber: dbRow[8]
|
||||
};
|
||||
if (row.type === 'DisplayName') {
|
||||
row.previousDisplayName = dbRow[5];
|
||||
} else if (row.type === 'TrustLevel') {
|
||||
row.trustLevel = dbRow[6];
|
||||
row.previousTrustLevel = dbRow[7];
|
||||
}
|
||||
friendLogHistory.push(row);
|
||||
},
|
||||
`SELECT * FROM ${dbVars.userPrefix}_friend_log_history WHERE user_id = @user_id${typeFilter}`,
|
||||
{
|
||||
'@user_id': userId
|
||||
}
|
||||
);
|
||||
return friendLogHistory;
|
||||
},
|
||||
|
||||
deleteFriendLogHistory(rowId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_friend_log_history WHERE id = @row_id`,
|
||||
{
|
||||
'@row_id': rowId
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { friendLogHistory };
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,171 @@
|
||||
import { avatarFavorites } from './avatarFavorites.js';
|
||||
import { avatarTags } from './avatarTags.js';
|
||||
import { feed } from './feed.js';
|
||||
import { friendFavorites } from './friendFavorites.js';
|
||||
import { friendLogCurrent } from './friendLogCurrent.js';
|
||||
import { friendLogHistory } from './friendLogHistory.js';
|
||||
import { gameLog } from './gameLog.js';
|
||||
import { memos } from './memos.js';
|
||||
import { moderation } from './moderation.js';
|
||||
import { mutualGraph } from './mutualGraph.js';
|
||||
import { notifications } from './notifications.js';
|
||||
import { tableAlter } from './tableAlter.js';
|
||||
import { tableFixes } from './tableFixes.js';
|
||||
import { tableSize } from './tableSize.js';
|
||||
import { worldFavorites } from './worldFavorites.js';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const dbVars = {
|
||||
userId: '',
|
||||
userPrefix: '',
|
||||
maxTableSize: 500,
|
||||
searchTableSize: 5000
|
||||
};
|
||||
|
||||
const database = {
|
||||
...feed,
|
||||
...gameLog,
|
||||
...notifications,
|
||||
...moderation,
|
||||
...friendLogHistory,
|
||||
...friendLogCurrent,
|
||||
...memos,
|
||||
...avatarFavorites,
|
||||
...avatarTags,
|
||||
...friendFavorites,
|
||||
...worldFavorites,
|
||||
...tableAlter,
|
||||
...tableFixes,
|
||||
...tableSize,
|
||||
...mutualGraph,
|
||||
|
||||
setMaxTableSize(limit) {
|
||||
dbVars.maxTableSize = limit;
|
||||
},
|
||||
|
||||
setSearchTableSize(limit) {
|
||||
dbVars.searchTableSize = limit;
|
||||
},
|
||||
|
||||
async initUserTables(userId) {
|
||||
dbVars.userId = userId;
|
||||
dbVars.userPrefix = userId.replaceAll('-', '').replaceAll('_', '');
|
||||
// Fix escape, add underscore if prefix starts with a number
|
||||
if (dbVars.userPrefix.match(/^\d/)) {
|
||||
dbVars.userPrefix = '_' + dbVars.userPrefix;
|
||||
}
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_gps (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, location TEXT, world_name TEXT, previous_location TEXT, time INTEGER, group_name TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_status (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, status TEXT, status_description TEXT, previous_status TEXT, previous_status_description TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_bio (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, bio TEXT, previous_bio TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_avatar (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, owner_id TEXT, avatar_name TEXT, current_avatar_image_url TEXT, current_avatar_thumbnail_image_url TEXT, previous_current_avatar_image_url TEXT, previous_current_avatar_thumbnail_image_url TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_online_offline (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, type TEXT, location TEXT, world_name TEXT, time INTEGER, group_name TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_friend_log_current (user_id TEXT PRIMARY KEY, display_name TEXT, trust_level TEXT, friend_number INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_friend_log_history (id INTEGER PRIMARY KEY, created_at TEXT, type TEXT, user_id TEXT, display_name TEXT, previous_display_name TEXT, trust_level TEXT, previous_trust_level TEXT, friend_number INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_notifications (id TEXT PRIMARY KEY, created_at TEXT, type TEXT, sender_user_id TEXT, sender_username TEXT, receiver_user_id TEXT, message TEXT, world_id TEXT, world_name TEXT, image_url TEXT, invite_message TEXT, request_message TEXT, response_message TEXT, expired INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_notifications_v2 (id TEXT PRIMARY KEY, created_at TEXT, updated_at TEXT, expires_at TEXT, type TEXT, link TEXT, link_text TEXT, message TEXT, title TEXT, image_url TEXT, seen INTEGER, sender_user_id TEXT, sender_username TEXT, data TEXT, responses TEXT, details TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_moderation (user_id TEXT PRIMARY KEY, updated_at TEXT, display_name TEXT, block INTEGER, mute INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_avatar_history (avatar_id TEXT PRIMARY KEY, created_at TEXT, time INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_notes (user_id TEXT PRIMARY KEY, display_name TEXT, note TEXT, created_at TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_mutual_graph_friends (friend_id TEXT PRIMARY KEY)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_mutual_graph_links (friend_id TEXT NOT NULL, mutual_id TEXT NOT NULL, PRIMARY KEY(friend_id, mutual_id))`
|
||||
);
|
||||
},
|
||||
|
||||
async initTables() {
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_location (id INTEGER PRIMARY KEY, created_at TEXT, location TEXT, world_id TEXT, world_name TEXT, time INTEGER, group_name TEXT, UNIQUE(created_at, location))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_join_leave (id INTEGER PRIMARY KEY, created_at TEXT, type TEXT, display_name TEXT, location TEXT, user_id TEXT, time INTEGER, UNIQUE(created_at, type, display_name))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_portal_spawn (id INTEGER PRIMARY KEY, created_at TEXT, display_name TEXT, location TEXT, user_id TEXT, instance_id TEXT, world_name TEXT, UNIQUE(created_at, display_name))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_video_play (id INTEGER PRIMARY KEY, created_at TEXT, video_url TEXT, video_name TEXT, video_id TEXT, location TEXT, display_name TEXT, user_id TEXT, UNIQUE(created_at, video_url))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_resource_load (id INTEGER PRIMARY KEY, created_at TEXT, resource_url TEXT, resource_type TEXT, location TEXT, UNIQUE(created_at, resource_url))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_event (id INTEGER PRIMARY KEY, created_at TEXT, data TEXT, UNIQUE(created_at, data))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS gamelog_external (id INTEGER PRIMARY KEY, created_at TEXT, message TEXT, display_name TEXT, user_id TEXT, location TEXT, UNIQUE(created_at, message))`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS cache_avatar (id TEXT PRIMARY KEY, added_at TEXT, author_id TEXT, author_name TEXT, created_at TEXT, description TEXT, image_url TEXT, name TEXT, release_status TEXT, thumbnail_image_url TEXT, updated_at TEXT, version INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS cache_world (id TEXT PRIMARY KEY, added_at TEXT, author_id TEXT, author_name TEXT, created_at TEXT, description TEXT, image_url TEXT, name TEXT, release_status TEXT, thumbnail_image_url TEXT, updated_at TEXT, version INTEGER)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS favorite_world (id INTEGER PRIMARY KEY, created_at TEXT, world_id TEXT, group_name TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS favorite_avatar (id INTEGER PRIMARY KEY, created_at TEXT, avatar_id TEXT, group_name TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS favorite_friend (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, group_name TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS memos (user_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS world_memos (world_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS avatar_memos (avatar_id TEXT PRIMARY KEY, edited_at TEXT, memo TEXT)`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`CREATE TABLE IF NOT EXISTS avatar_tags (avatar_id TEXT NOT NULL, tag TEXT NOT NULL, color TEXT, PRIMARY KEY (avatar_id, tag))`
|
||||
);
|
||||
},
|
||||
|
||||
begin() {
|
||||
sqliteService.executeNonQuery('BEGIN');
|
||||
},
|
||||
|
||||
commit() {
|
||||
sqliteService.executeNonQuery('COMMIT');
|
||||
},
|
||||
|
||||
async vacuum() {
|
||||
await sqliteService.executeNonQuery('VACUUM');
|
||||
},
|
||||
|
||||
async optimize() {
|
||||
await sqliteService.executeNonQuery('PRAGMA optimize');
|
||||
}
|
||||
};
|
||||
|
||||
window.database = database;
|
||||
export { database, dbVars };
|
||||
@@ -0,0 +1,176 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const memos = {
|
||||
// user memos
|
||||
|
||||
async getUserMemo(userId) {
|
||||
var row = {};
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
userId: dbRow[0],
|
||||
editedAt: dbRow[1],
|
||||
memo: dbRow[2]
|
||||
};
|
||||
},
|
||||
`SELECT * FROM memos WHERE user_id = @user_id`,
|
||||
{
|
||||
'@user_id': userId
|
||||
}
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
async getAllUserMemos() {
|
||||
var memos = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
userId: dbRow[0],
|
||||
memo: dbRow[1]
|
||||
};
|
||||
memos.push(row);
|
||||
}, 'SELECT user_id, memo FROM memos');
|
||||
return memos;
|
||||
},
|
||||
|
||||
async setUserMemo(entry) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO memos (user_id, edited_at, memo) VALUES (@user_id, @edited_at, @memo)`,
|
||||
{
|
||||
'@user_id': entry.userId,
|
||||
'@edited_at': entry.editedAt,
|
||||
'@memo': entry.memo
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async deleteUserMemo(userId) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM memos WHERE user_id = @user_id`,
|
||||
{
|
||||
'@user_id': userId
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// world memos
|
||||
|
||||
async getWorldMemo(worldId) {
|
||||
var row = {};
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
worldId: dbRow[0],
|
||||
editedAt: dbRow[1],
|
||||
memo: dbRow[2]
|
||||
};
|
||||
},
|
||||
`SELECT * FROM world_memos WHERE world_id = @world_id`,
|
||||
{
|
||||
'@world_id': worldId
|
||||
}
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
setWorldMemo(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO world_memos (world_id, edited_at, memo) VALUES (@world_id, @edited_at, @memo)`,
|
||||
{
|
||||
'@world_id': entry.worldId,
|
||||
'@edited_at': entry.editedAt,
|
||||
'@memo': entry.memo
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteWorldMemo(worldId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM world_memos WHERE world_id = @world_id`,
|
||||
{
|
||||
'@world_id': worldId
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// Avatar memos
|
||||
|
||||
async getAvatarMemoDB(avatarId) {
|
||||
var row = {};
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
row = {
|
||||
avatarId: dbRow[0],
|
||||
editedAt: dbRow[1],
|
||||
memo: dbRow[2]
|
||||
};
|
||||
},
|
||||
`SELECT * FROM avatar_memos WHERE avatar_id = @avatar_id`,
|
||||
{
|
||||
'@avatar_id': avatarId
|
||||
}
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
setAvatarMemo(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO avatar_memos (avatar_id, edited_at, memo) VALUES (@avatar_id, @edited_at, @memo)`,
|
||||
{
|
||||
'@avatar_id': entry.avatarId,
|
||||
'@edited_at': entry.editedAt,
|
||||
'@memo': entry.memo
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteAvatarMemo(avatarId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM avatar_memos WHERE avatar_id = @avatar_id`,
|
||||
{
|
||||
'@avatar_id': avatarId
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// user notes
|
||||
|
||||
async addUserNote(note) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_notes (user_id, display_name, note, created_at) VALUES (@user_id, @display_name, @note, @created_at)`,
|
||||
{
|
||||
'@user_id': note.userId,
|
||||
'@display_name': note.displayName,
|
||||
'@note': note.note,
|
||||
'@created_at': note.createdAt
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getAllUserNotes() {
|
||||
var data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
userId: dbRow[0],
|
||||
displayName: dbRow[1],
|
||||
note: dbRow[2],
|
||||
createdAt: dbRow[3]
|
||||
};
|
||||
data.push(row);
|
||||
}, `SELECT user_id, display_name, note, created_at FROM ${dbVars.userPrefix}_notes`);
|
||||
return data;
|
||||
},
|
||||
|
||||
async deleteUserNote(userId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_notes WHERE user_id = @userId`,
|
||||
{
|
||||
'@userId': userId
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { memos };
|
||||
@@ -0,0 +1,65 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const moderation = {
|
||||
async getModeration(userId) {
|
||||
var row = {};
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
var block = false;
|
||||
var mute = false;
|
||||
if (dbRow[3] === 1) {
|
||||
block = true;
|
||||
}
|
||||
if (dbRow[4] === 1) {
|
||||
mute = true;
|
||||
}
|
||||
row = {
|
||||
userId: dbRow[0],
|
||||
updatedAt: dbRow[1],
|
||||
displayName: dbRow[2],
|
||||
block,
|
||||
mute
|
||||
};
|
||||
},
|
||||
`SELECT * FROM ${dbVars.userPrefix}_moderation WHERE user_id = @userId`,
|
||||
{
|
||||
'@userId': userId
|
||||
}
|
||||
);
|
||||
return row;
|
||||
},
|
||||
|
||||
setModeration(entry) {
|
||||
var block = 0;
|
||||
var mute = 0;
|
||||
if (entry.block) {
|
||||
block = 1;
|
||||
}
|
||||
if (entry.mute) {
|
||||
mute = 1;
|
||||
}
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_moderation (user_id, updated_at, display_name, block, mute) VALUES (@user_id, @updated_at, @display_name, @block, @mute)`,
|
||||
{
|
||||
'@user_id': entry.userId,
|
||||
'@updated_at': entry.updatedAt,
|
||||
'@display_name': entry.displayName,
|
||||
'@block': block,
|
||||
'@mute': mute
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteModeration(userId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_moderation WHERE user_id = @user_id`,
|
||||
{
|
||||
'@user_id': userId
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { moderation };
|
||||
@@ -0,0 +1,137 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const mutualGraph = {
|
||||
async getMutualGraphSnapshot() {
|
||||
const snapshot = new Map();
|
||||
if (!dbVars.userPrefix) {
|
||||
return snapshot;
|
||||
}
|
||||
const friendTable = `${dbVars.userPrefix}_mutual_graph_friends`;
|
||||
const linkTable = `${dbVars.userPrefix}_mutual_graph_links`;
|
||||
await sqliteService.execute((dbRow) => {
|
||||
const friendId = dbRow[0];
|
||||
if (friendId && !snapshot.has(friendId)) {
|
||||
snapshot.set(friendId, []);
|
||||
}
|
||||
}, `SELECT friend_id FROM ${friendTable}`);
|
||||
await sqliteService.execute((dbRow) => {
|
||||
const friendId = dbRow[0];
|
||||
const mutualId = dbRow[1];
|
||||
if (!friendId || !mutualId) {
|
||||
return;
|
||||
}
|
||||
let list = snapshot.get(friendId);
|
||||
if (!list) {
|
||||
list = [];
|
||||
snapshot.set(friendId, list);
|
||||
}
|
||||
list.push(mutualId);
|
||||
}, `SELECT friend_id, mutual_id FROM ${linkTable}`);
|
||||
return snapshot;
|
||||
},
|
||||
|
||||
async saveMutualGraphSnapshot(entries) {
|
||||
if (!dbVars.userPrefix) {
|
||||
return;
|
||||
}
|
||||
const friendTable = `${dbVars.userPrefix}_mutual_graph_friends`;
|
||||
const linkTable = `${dbVars.userPrefix}_mutual_graph_links`;
|
||||
const pairs = entries instanceof Map ? entries : new Map();
|
||||
await sqliteService.executeNonQuery('BEGIN');
|
||||
try {
|
||||
await sqliteService.executeNonQuery(`DELETE FROM ${friendTable}`);
|
||||
await sqliteService.executeNonQuery(`DELETE FROM ${linkTable}`);
|
||||
if (pairs.size === 0) {
|
||||
await sqliteService.executeNonQuery('COMMIT');
|
||||
return;
|
||||
}
|
||||
let friendValues = '';
|
||||
let edgeValues = '';
|
||||
pairs.forEach((mutualIds, friendId) => {
|
||||
if (!friendId) {
|
||||
return;
|
||||
}
|
||||
const safeFriendId = friendId.replace(/'/g, "''");
|
||||
friendValues += `('${safeFriendId}'),`;
|
||||
let collection = [];
|
||||
if (Array.isArray(mutualIds)) {
|
||||
collection = mutualIds;
|
||||
} else if (mutualIds instanceof Set) {
|
||||
collection = Array.from(mutualIds);
|
||||
}
|
||||
for (const mutual of collection) {
|
||||
if (!mutual) {
|
||||
continue;
|
||||
}
|
||||
const safeMutualId = String(mutual).replace(/'/g, "''");
|
||||
edgeValues += `('${safeFriendId}', '${safeMutualId}'),`;
|
||||
}
|
||||
});
|
||||
if (friendValues) {
|
||||
friendValues = friendValues.slice(0, -1);
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${friendTable} (friend_id) VALUES ${friendValues}`
|
||||
);
|
||||
}
|
||||
if (edgeValues) {
|
||||
edgeValues = edgeValues.slice(0, -1);
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${linkTable} (friend_id, mutual_id) VALUES ${edgeValues}`
|
||||
);
|
||||
}
|
||||
await sqliteService.executeNonQuery('COMMIT');
|
||||
} catch (err) {
|
||||
await sqliteService.executeNonQuery('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
async updateMutualsForFriend(friendId, mutualIds) {
|
||||
if (!dbVars.userPrefix || !friendId) {
|
||||
return;
|
||||
}
|
||||
const friendTable = `${dbVars.userPrefix}_mutual_graph_friends`;
|
||||
const linkTable = `${dbVars.userPrefix}_mutual_graph_links`;
|
||||
const safeFriendId = friendId.replace(/'/g, "''");
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${friendTable} (friend_id) VALUES ('${safeFriendId}')`
|
||||
);
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${linkTable} WHERE friend_id='${safeFriendId}'`
|
||||
);
|
||||
let edgeValues = '';
|
||||
for (const mutual of mutualIds) {
|
||||
if (!mutual) {
|
||||
continue;
|
||||
}
|
||||
const safeMutualId = String(mutual).replace(/'/g, "''");
|
||||
edgeValues += `('${safeFriendId}', '${safeMutualId}'),`;
|
||||
}
|
||||
if (edgeValues) {
|
||||
edgeValues = edgeValues.slice(0, -1);
|
||||
await sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${linkTable} (friend_id, mutual_id) VALUES ${edgeValues}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async getMutualCountForAllUsers() {
|
||||
const mutualCountMap = new Map();
|
||||
if (!dbVars.userPrefix) {
|
||||
return mutualCountMap;
|
||||
}
|
||||
const linkTable = `${dbVars.userPrefix}_mutual_graph_links`;
|
||||
await sqliteService.execute((dbRow) => {
|
||||
const mutualId = dbRow[0];
|
||||
const count = dbRow[1];
|
||||
if (mutualId) {
|
||||
mutualCountMap.set(mutualId, count);
|
||||
}
|
||||
}, `SELECT mutual_id, COUNT(*) FROM ${linkTable} GROUP BY mutual_id`);
|
||||
return mutualCountMap;
|
||||
}
|
||||
};
|
||||
|
||||
export { mutualGraph };
|
||||
@@ -0,0 +1,240 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const notifications = {
|
||||
async getNotifications() {
|
||||
var notifications = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
id: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
type: dbRow[2],
|
||||
senderUserId: dbRow[3],
|
||||
senderUsername: dbRow[4],
|
||||
receiverUserId: dbRow[5],
|
||||
message: dbRow[6],
|
||||
details: {
|
||||
worldId: dbRow[7],
|
||||
worldName: dbRow[8],
|
||||
imageUrl: dbRow[9],
|
||||
inviteMessage: dbRow[10],
|
||||
requestMessage: dbRow[11],
|
||||
responseMessage: dbRow[12]
|
||||
},
|
||||
$isExpired: dbRow[13] === 1
|
||||
};
|
||||
notifications.unshift(row);
|
||||
}, `SELECT * FROM ${dbVars.userPrefix}_notifications ORDER BY created_at DESC LIMIT ${dbVars.maxTableSize}`);
|
||||
return notifications;
|
||||
},
|
||||
|
||||
async lookupNotificationDatabase(
|
||||
search,
|
||||
filters,
|
||||
vipList,
|
||||
maxEntries = dbVars.maxTableSize
|
||||
) {
|
||||
search = search.replaceAll("'", "''");
|
||||
let notifications = [];
|
||||
|
||||
let vipQuery = '';
|
||||
if (vipList.length > 0) {
|
||||
const vipIds = vipList.map(
|
||||
(userId) => `'${userId.replaceAll("'", "''")}'`
|
||||
);
|
||||
vipQuery = `AND sender_user_id IN (${vipIds.join(',')})`;
|
||||
}
|
||||
|
||||
let filterQuery = '';
|
||||
if (filters.length > 0) {
|
||||
const filterTypes = filters.map(
|
||||
(type) => `'${type.replaceAll("'", "''")}'`
|
||||
);
|
||||
filterQuery = `AND type IN (${filterTypes.join(',')})`;
|
||||
}
|
||||
|
||||
await sqliteService.execute((dbRow) => {
|
||||
let row = {
|
||||
id: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
type: dbRow[2],
|
||||
senderUserId: dbRow[3],
|
||||
senderUsername: dbRow[4],
|
||||
receiverUserId: dbRow[5],
|
||||
message: dbRow[6],
|
||||
details: {
|
||||
worldId: dbRow[7],
|
||||
worldName: dbRow[8],
|
||||
imageUrl: dbRow[9],
|
||||
inviteMessage: dbRow[10],
|
||||
requestMessage: dbRow[11],
|
||||
responseMessage: dbRow[12]
|
||||
},
|
||||
$isExpired: dbRow[13] === 1
|
||||
};
|
||||
notifications.unshift(row);
|
||||
}, `SELECT * FROM ${dbVars.userPrefix}_notifications WHERE (sender_username LIKE '%${search}%' OR message LIKE '%${search}%' OR world_name LIKE '%${search}%') ${vipQuery} ${filterQuery} ORDER BY created_at DESC LIMIT ${maxEntries}`);
|
||||
return notifications;
|
||||
},
|
||||
|
||||
addNotificationToDatabase(row) {
|
||||
var entry = {
|
||||
id: '',
|
||||
created_at: '',
|
||||
type: '',
|
||||
senderUserId: '',
|
||||
senderUsername: '',
|
||||
receiverUserId: '',
|
||||
message: '',
|
||||
...row,
|
||||
details: {
|
||||
worldId: '',
|
||||
worldName: '',
|
||||
imageUrl: '',
|
||||
inviteMessage: '',
|
||||
requestMessage: '',
|
||||
responseMessage: '',
|
||||
...row.details
|
||||
}
|
||||
};
|
||||
if (entry.imageUrl && !entry.details.imageUrl) {
|
||||
entry.details.imageUrl = entry.imageUrl;
|
||||
}
|
||||
var expired = 0;
|
||||
if (row.$isExpired) {
|
||||
expired = 1;
|
||||
}
|
||||
if (!entry.created_at || !entry.type || !entry.id) {
|
||||
console.error('Notification is missing required field', entry);
|
||||
throw new Error('Notification is missing required field');
|
||||
}
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR IGNORE INTO ${dbVars.userPrefix}_notifications (id, created_at, type, sender_user_id, sender_username, receiver_user_id, message, world_id, world_name, image_url, invite_message, request_message, response_message, expired) VALUES (@id, @created_at, @type, @sender_user_id, @sender_username, @receiver_user_id, @message, @world_id, @world_name, @image_url, @invite_message, @request_message, @response_message, @expired)`,
|
||||
{
|
||||
'@id': entry.id,
|
||||
'@created_at': entry.created_at,
|
||||
'@type': entry.type,
|
||||
'@sender_user_id': entry.senderUserId,
|
||||
'@sender_username': entry.senderUsername,
|
||||
'@receiver_user_id': entry.receiverUserId,
|
||||
'@message': entry.message,
|
||||
'@world_id': entry.details.worldId,
|
||||
'@world_name': entry.details.worldName,
|
||||
'@image_url': entry.details.imageUrl,
|
||||
'@invite_message': entry.details.inviteMessage,
|
||||
'@request_message': entry.details.requestMessage,
|
||||
'@response_message': entry.details.responseMessage,
|
||||
'@expired': expired
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteNotification(rowId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_notifications WHERE id = @row_id`,
|
||||
{
|
||||
'@row_id': rowId
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
updateNotificationExpired(entry) {
|
||||
var expired = 0;
|
||||
if (entry.$isExpired) {
|
||||
expired = 1;
|
||||
}
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE ${dbVars.userPrefix}_notifications SET expired = @expired WHERE id = @id`,
|
||||
{
|
||||
'@id': entry.id,
|
||||
'@expired': expired
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// notifications v2
|
||||
|
||||
async getNotificationsV2() {
|
||||
const notifications = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
const row = {
|
||||
id: dbRow[0],
|
||||
createdAt: dbRow[1],
|
||||
updatedAt: dbRow[2],
|
||||
expiresAt: dbRow[3],
|
||||
type: dbRow[4],
|
||||
link: dbRow[5],
|
||||
linkText: dbRow[6],
|
||||
message: dbRow[7],
|
||||
title: dbRow[8],
|
||||
imageUrl: dbRow[9],
|
||||
seen: dbRow[10] === 1,
|
||||
senderUserId: dbRow[11],
|
||||
senderUsername: dbRow[12],
|
||||
data: JSON.parse(dbRow[13] || '{}'),
|
||||
responses: JSON.parse(dbRow[14] || '[]'),
|
||||
details: JSON.parse(dbRow[15] || '{}')
|
||||
};
|
||||
// for UI table
|
||||
row.created_at = row.createdAt;
|
||||
row.version = 2;
|
||||
notifications.unshift(row);
|
||||
}, `SELECT * FROM ${dbVars.userPrefix}_notifications_v2 ORDER BY created_at DESC LIMIT ${dbVars.maxTableSize}`);
|
||||
return notifications;
|
||||
},
|
||||
|
||||
addNotificationV2ToDatabase(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO ${dbVars.userPrefix}_notifications_v2 (id, created_at, updated_at, expires_at, type, link, link_text, message, title, image_url, seen, sender_user_id, sender_username, data, responses, details) VALUES (@id, @created_at, @updated_at, @expires_at, @type, @link, @link_text, @message, @title, @image_url, @seen, @sender_user_id, @sender_username, @data, @responses, @details)`,
|
||||
{
|
||||
'@id': entry.id,
|
||||
'@created_at': entry.createdAt,
|
||||
'@updated_at': entry.updatedAt,
|
||||
'@expires_at': entry.expiresAt,
|
||||
'@type': entry.type,
|
||||
'@link': entry.link,
|
||||
'@link_text': entry.linkText,
|
||||
'@message': entry.message,
|
||||
'@title': entry.title,
|
||||
'@image_url': entry.imageUrl,
|
||||
'@seen': entry.seen ? 1 : 0,
|
||||
'@sender_user_id': entry.senderUserId,
|
||||
'@sender_username': entry.senderUsername,
|
||||
'@data': JSON.stringify(entry.data || {}),
|
||||
'@responses': JSON.stringify(entry.responses || []),
|
||||
'@details': JSON.stringify(entry.details || {})
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
expireNotificationV2(id) {
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE ${dbVars.userPrefix}_notifications_v2 SET expires_at = @expires_at, seen = 1 WHERE id = @id`,
|
||||
{
|
||||
'@id': id,
|
||||
'@expires_at': new Date().toJSON()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
seenNotificationV2(id) {
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE ${dbVars.userPrefix}_notifications_v2 SET seen = 1 WHERE id = @id`,
|
||||
{
|
||||
'@id': id
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteNotificationV2(id) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${dbVars.userPrefix}_notifications_v2 WHERE id = @id`,
|
||||
{
|
||||
'@id': id
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export { notifications };
|
||||
@@ -0,0 +1,86 @@
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const tableAlter = {
|
||||
async upgradeDatabaseVersion() {
|
||||
// var version = 0;
|
||||
// await sqliteService.execute((dbRow) => {
|
||||
// version = dbRow[0];
|
||||
// }, 'PRAGMA user_version');
|
||||
// if (version === 0) {
|
||||
await this.updateTableForGroupNames();
|
||||
await this.addFriendLogFriendNumber();
|
||||
await this.updateTableForAvatarHistory();
|
||||
// }
|
||||
// await sqliteService.executeNonQuery('PRAGMA user_version = 1');
|
||||
},
|
||||
|
||||
async updateTableForGroupNames() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_feed_gps' OR name LIKE '%_feed_online_offline' OR name = 'gamelog_location'`);
|
||||
for (var tableName of tables) {
|
||||
try {
|
||||
await sqliteService.executeNonQuery(
|
||||
`ALTER TABLE ${tableName} ADD group_name TEXT DEFAULT ''`
|
||||
);
|
||||
} catch (e) {
|
||||
e = e.toString();
|
||||
if (e.indexOf('duplicate column name') === -1) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fix gamelog_location column typo
|
||||
try {
|
||||
await sqliteService.executeNonQuery(
|
||||
`ALTER TABLE gamelog_location DROP COLUMN groupName`
|
||||
);
|
||||
} catch (e) {
|
||||
e = e.toString();
|
||||
if (e.indexOf('no such column') === -1) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async addFriendLogFriendNumber() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_friend_log_current' OR name LIKE '%_friend_log_history'`);
|
||||
for (var tableName of tables) {
|
||||
try {
|
||||
await sqliteService.executeNonQuery(
|
||||
`ALTER TABLE ${tableName} ADD friend_number INTEGER DEFAULT 0`
|
||||
);
|
||||
} catch (e) {
|
||||
e = e.toString();
|
||||
if (e.indexOf('duplicate column name') === -1) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async updateTableForAvatarHistory() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_avatar_history'`);
|
||||
for (var tableName of tables) {
|
||||
try {
|
||||
await sqliteService.executeNonQuery(
|
||||
`ALTER TABLE ${tableName} ADD time INTEGER DEFAULT 0`
|
||||
);
|
||||
} catch (e) {
|
||||
e = e.toString();
|
||||
if (e.indexOf('duplicate column name') === -1) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { tableAlter };
|
||||
@@ -0,0 +1,181 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const tableFixes = {
|
||||
async cleanLegendFromFriendLog() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_friend_log_history'`);
|
||||
for (var tableName of tables) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${tableName}
|
||||
WHERE type = 'TrustLevel' AND created_at > '2022-05-04T01:00:00.000Z'
|
||||
AND ((trust_level = 'Veteran User' AND previous_trust_level = 'Trusted User') OR (trust_level = 'Trusted User' AND previous_trust_level = 'Veteran User'))`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async fixGameLogTraveling() {
|
||||
var travelingList = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
rowId: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
type: dbRow[2],
|
||||
displayName: dbRow[3],
|
||||
location: dbRow[4],
|
||||
userId: dbRow[5],
|
||||
time: dbRow[6]
|
||||
};
|
||||
travelingList.unshift(row);
|
||||
}, "SELECT * FROM gamelog_join_leave WHERE type = 'OnPlayerLeft' AND location = 'traveling'");
|
||||
travelingList.forEach(async (travelingEntry) => {
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
var onPlayingJoin = {
|
||||
rowId: dbRow[0],
|
||||
created_at: dbRow[1],
|
||||
type: dbRow[2],
|
||||
displayName: dbRow[3],
|
||||
location: dbRow[4],
|
||||
userId: dbRow[5],
|
||||
time: dbRow[6]
|
||||
};
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE gamelog_join_leave SET location = @location WHERE id = @rowId`,
|
||||
{
|
||||
'@rowId': travelingEntry.rowId,
|
||||
'@location': onPlayingJoin.location
|
||||
}
|
||||
);
|
||||
},
|
||||
"SELECT * FROM gamelog_join_leave WHERE type = 'OnPlayerJoined' AND display_name = @displayName AND created_at <= @created_at ORDER BY created_at DESC LIMIT 1",
|
||||
{
|
||||
'@displayName': travelingEntry.displayName,
|
||||
'@created_at': travelingEntry.created_at
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
async fixNegativeGPS() {
|
||||
var gpsTables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
gpsTables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_gps'`);
|
||||
gpsTables.forEach((tableName) => {
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE ${tableName} SET time = 0 WHERE time < 0`
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
async getBrokenLeaveEntries() {
|
||||
var instances = await this.getGameLogInstancesTime();
|
||||
var badEntries = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
if (typeof dbRow[1] === 'number') {
|
||||
var ref = instances.get(dbRow[0]);
|
||||
if (typeof ref !== 'undefined' && dbRow[1] > ref) {
|
||||
badEntries.push(dbRow[2]);
|
||||
}
|
||||
}
|
||||
}, `SELECT location, time, id FROM gamelog_join_leave WHERE type = 'OnPlayerLeft' AND time > 0`);
|
||||
return badEntries;
|
||||
},
|
||||
|
||||
async fixBrokenLeaveEntries() {
|
||||
var badEntries = await this.getBrokenLeaveEntries();
|
||||
var badEntriesList = '';
|
||||
var count = badEntries.length;
|
||||
badEntries.forEach((entry) => {
|
||||
count--;
|
||||
if (count === 0) {
|
||||
badEntriesList = badEntriesList.concat(entry);
|
||||
} else {
|
||||
badEntriesList = badEntriesList.concat(`${entry}, `);
|
||||
}
|
||||
});
|
||||
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE gamelog_join_leave SET time = 0 WHERE id IN (${badEntriesList})`
|
||||
);
|
||||
},
|
||||
|
||||
async fixBrokenGroupInvites() {
|
||||
var notificationTables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
notificationTables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_notifications'`);
|
||||
notificationTables.forEach((tableName) => {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${tableName} WHERE type LIKE '%.%'`
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
async fixBrokenNotifications() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_notifications'`);
|
||||
for (var tableName of tables) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${tableName} WHERE (created_at is null or created_at = '')`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async fixBrokenGroupChange() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_notifications'`);
|
||||
for (var tableName of tables) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`DELETE FROM ${tableName} WHERE type = 'groupChange' AND created_at < '2024-04-23T03:00:00.000Z'`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async fixCancelFriendRequestTypo() {
|
||||
var tables = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
tables.push(dbRow[0]);
|
||||
}, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_friend_log_history'`);
|
||||
for (var tableName of tables) {
|
||||
await sqliteService.executeNonQuery(
|
||||
`UPDATE ${tableName} SET type = 'CancelFriendRequest' WHERE type = 'CancelFriendRequst'`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async getBrokenGameLogDisplayNames() {
|
||||
var badEntries = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
badEntries.push({
|
||||
id: dbRow[0],
|
||||
displayName: dbRow[1]
|
||||
});
|
||||
}, "SELECT id, display_name FROM gamelog_join_leave WHERE display_name LIKE '% (%'");
|
||||
return badEntries;
|
||||
},
|
||||
|
||||
async fixBrokenGameLogDisplayNames() {
|
||||
var badEntries = await this.getBrokenGameLogDisplayNames();
|
||||
badEntries.forEach((entry) => {
|
||||
var newDisplayName = entry.displayName.split(' (')[0];
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE gamelog_join_leave SET display_name = @new_display_name WHERE id = @id`,
|
||||
{
|
||||
'@new_display_name': newDisplayName,
|
||||
'@id': entry.id
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export { tableFixes };
|
||||
@@ -0,0 +1,127 @@
|
||||
import { dbVars } from '../database';
|
||||
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const tableSize = {
|
||||
async getMaxFriendLogNumber() {
|
||||
var friendNumber = 0;
|
||||
await sqliteService.execute((dbRow) => {
|
||||
friendNumber = dbRow[0];
|
||||
}, `SELECT MAX(friend_number) FROM ${dbVars.userPrefix}_friend_log_current`);
|
||||
return friendNumber;
|
||||
},
|
||||
|
||||
async getGpsTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM ${dbVars.userPrefix}_feed_gps`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getStatusTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM ${dbVars.userPrefix}_feed_status`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getBioTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM ${dbVars.userPrefix}_feed_bio`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getAvatarTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM ${dbVars.userPrefix}_feed_avatar`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getOnlineOfflineTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM ${dbVars.userPrefix}_feed_online_offline`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getFriendLogHistoryTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM ${dbVars.userPrefix}_friend_log_history`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getNotificationTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM ${dbVars.userPrefix}_notifications`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getLocationTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM gamelog_location`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getJoinLeaveTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM gamelog_join_leave`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getPortalSpawnTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM gamelog_portal_spawn`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getVideoPlayTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM gamelog_video_play`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getResourceLoadTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM gamelog_resource_load`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getEventTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM gamelog_event`);
|
||||
return size;
|
||||
},
|
||||
|
||||
async getExternalTableSize() {
|
||||
var size = 0;
|
||||
await sqliteService.execute((row) => {
|
||||
size = row[0];
|
||||
}, `SELECT COUNT(*) FROM gamelog_external`);
|
||||
return size;
|
||||
}
|
||||
};
|
||||
|
||||
export { tableSize };
|
||||
@@ -0,0 +1,136 @@
|
||||
import sqliteService from '../sqlite.js';
|
||||
|
||||
const worldFavorites = {
|
||||
addWorldToCache(entry) {
|
||||
sqliteService.executeNonQuery(
|
||||
`INSERT OR REPLACE INTO cache_world (id, added_at, author_id, author_name, created_at, description, image_url, name, release_status, thumbnail_image_url, updated_at, version) VALUES (@id, @added_at, @author_id, @author_name, @created_at, @description, @image_url, @name, @release_status, @thumbnail_image_url, @updated_at, @version)`,
|
||||
{
|
||||
'@id': entry.id,
|
||||
'@added_at': new Date().toJSON(),
|
||||
'@author_id': entry.authorId,
|
||||
'@author_name': entry.authorName,
|
||||
'@created_at': entry.created_at,
|
||||
'@description': entry.description,
|
||||
'@image_url': entry.imageUrl,
|
||||
'@name': entry.name,
|
||||
'@release_status': entry.releaseStatus,
|
||||
'@thumbnail_image_url': entry.thumbnailImageUrl,
|
||||
'@updated_at': entry.updated_at,
|
||||
'@version': entry.version
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
addWorldToFavorites(worldId, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
'INSERT OR REPLACE INTO favorite_world (world_id, group_name, created_at) VALUES (@world_id, @group_name, @created_at)',
|
||||
{
|
||||
'@world_id': worldId,
|
||||
'@group_name': groupName,
|
||||
'@created_at': new Date().toJSON()
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
renameWorldFavoriteGroup(newGroupName, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`UPDATE favorite_world SET group_name = @new_group_name WHERE group_name = @group_name`,
|
||||
{
|
||||
'@new_group_name': newGroupName,
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
deleteWorldFavoriteGroup(groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM favorite_world WHERE group_name = @group_name`,
|
||||
{
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
removeWorldFromFavorites(worldId, groupName) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM favorite_world WHERE world_id = @world_id AND group_name = @group_name`,
|
||||
{
|
||||
'@world_id': worldId,
|
||||
'@group_name': groupName
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getWorldFavorites() {
|
||||
var data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
created_at: dbRow[1],
|
||||
worldId: dbRow[2],
|
||||
groupName: dbRow[3]
|
||||
};
|
||||
data.push(row);
|
||||
}, 'SELECT * FROM favorite_world');
|
||||
return data;
|
||||
},
|
||||
|
||||
removeWorldFromCache(worldId) {
|
||||
sqliteService.executeNonQuery(
|
||||
`DELETE FROM cache_world WHERE id = @world_id`,
|
||||
{
|
||||
'@world_id': worldId
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async getWorldCache() {
|
||||
var data = [];
|
||||
await sqliteService.execute((dbRow) => {
|
||||
var row = {
|
||||
id: dbRow[0],
|
||||
// added_at: dbRow[1],
|
||||
authorId: dbRow[2],
|
||||
authorName: dbRow[3],
|
||||
created_at: dbRow[4],
|
||||
description: dbRow[5],
|
||||
imageUrl: dbRow[6],
|
||||
name: dbRow[7],
|
||||
releaseStatus: dbRow[8],
|
||||
thumbnailImageUrl: dbRow[9],
|
||||
updated_at: dbRow[10],
|
||||
version: dbRow[11]
|
||||
};
|
||||
data.push(row);
|
||||
}, 'SELECT * FROM cache_world');
|
||||
return data;
|
||||
},
|
||||
|
||||
async getCachedWorldById(id) {
|
||||
var data = null;
|
||||
await sqliteService.execute(
|
||||
(dbRow) => {
|
||||
data = {
|
||||
id: dbRow[0],
|
||||
// added_at: dbRow[1],
|
||||
authorId: dbRow[2],
|
||||
authorName: dbRow[3],
|
||||
created_at: dbRow[4],
|
||||
description: dbRow[5],
|
||||
imageUrl: dbRow[6],
|
||||
name: dbRow[7],
|
||||
releaseStatus: dbRow[8],
|
||||
thumbnailImageUrl: dbRow[9],
|
||||
updated_at: dbRow[10],
|
||||
version: dbRow[11]
|
||||
};
|
||||
},
|
||||
`SELECT * FROM cache_world WHERE id = @id`,
|
||||
{
|
||||
'@id': id
|
||||
}
|
||||
);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
export { worldFavorites };
|
||||
@@ -0,0 +1,132 @@
|
||||
// requires binding of LogWatcher
|
||||
|
||||
class GameLogService {
|
||||
parseRawGameLog(dt, type, args) {
|
||||
var gameLog = {
|
||||
dt,
|
||||
type
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'location':
|
||||
gameLog.location = args[0];
|
||||
gameLog.worldName = args[1];
|
||||
break;
|
||||
|
||||
case 'location-destination':
|
||||
gameLog.location = args[0];
|
||||
break;
|
||||
|
||||
case 'player-joined':
|
||||
gameLog.displayName = args[0];
|
||||
gameLog.userId = args[1];
|
||||
break;
|
||||
|
||||
case 'player-left':
|
||||
gameLog.displayName = args[0];
|
||||
gameLog.userId = args[1];
|
||||
break;
|
||||
|
||||
case 'notification':
|
||||
gameLog.json = args[0];
|
||||
break;
|
||||
|
||||
case 'portal-spawn':
|
||||
break;
|
||||
|
||||
case 'event':
|
||||
gameLog.event = args[0];
|
||||
break;
|
||||
|
||||
case 'video-play':
|
||||
gameLog.videoUrl = args[0];
|
||||
gameLog.displayName = args[1];
|
||||
break;
|
||||
|
||||
case 'resource-load-string':
|
||||
case 'resource-load-image':
|
||||
gameLog.resourceUrl = args[0];
|
||||
break;
|
||||
|
||||
case 'video-sync':
|
||||
gameLog.timestamp = args[0];
|
||||
break;
|
||||
|
||||
case 'vrcx':
|
||||
gameLog.data = args[0];
|
||||
break;
|
||||
|
||||
case 'api-request':
|
||||
gameLog.url = args[0];
|
||||
break;
|
||||
|
||||
case 'avatar-change':
|
||||
gameLog.displayName = args[0];
|
||||
gameLog.avatarName = args[1];
|
||||
break;
|
||||
|
||||
case 'photon-id':
|
||||
gameLog.displayName = args[0];
|
||||
gameLog.photonId = args[1];
|
||||
break;
|
||||
|
||||
case 'screenshot':
|
||||
gameLog.screenshotPath = args[0];
|
||||
break;
|
||||
|
||||
case 'vrc-quit':
|
||||
break;
|
||||
|
||||
case 'openvr-init':
|
||||
break;
|
||||
|
||||
case 'desktop-mode':
|
||||
break;
|
||||
|
||||
case 'udon-exception':
|
||||
gameLog.data = args[0];
|
||||
break;
|
||||
|
||||
case 'sticker-spawn':
|
||||
gameLog.userId = args[0];
|
||||
gameLog.displayName = args[1];
|
||||
gameLog.inventoryId = args[2];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return gameLog;
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
var gameLogs = [];
|
||||
var done = false;
|
||||
while (!done) {
|
||||
var rawGameLogs = await LogWatcher.Get();
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (var [fileName, dt, type, ...args] of rawGameLogs) {
|
||||
var gameLog = this.parseRawGameLog(dt, type, args);
|
||||
gameLogs.push(gameLog);
|
||||
}
|
||||
if (rawGameLogs.length === 0) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
return gameLogs;
|
||||
}
|
||||
|
||||
async setDateTill(dateTill) {
|
||||
await LogWatcher.SetDateTill(dateTill);
|
||||
}
|
||||
|
||||
async reset() {
|
||||
await LogWatcher.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
var self = new GameLogService();
|
||||
window.gameLogService = self;
|
||||
|
||||
export { self as default, GameLogService as LogWatcherService };
|
||||
@@ -0,0 +1,42 @@
|
||||
let VRCXStorage = {};
|
||||
|
||||
export default class {
|
||||
constructor(_VRCXStorage) {
|
||||
VRCXStorage = _VRCXStorage;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
VRCXStorage.GetArray = async function (key) {
|
||||
try {
|
||||
var array = JSON.parse(await this.Get(key));
|
||||
if (Array.isArray(array)) {
|
||||
return array;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
VRCXStorage.SetArray = function (key, value) {
|
||||
this.Set(key, JSON.stringify(value));
|
||||
};
|
||||
|
||||
VRCXStorage.GetObject = async function (key) {
|
||||
try {
|
||||
var object = JSON.parse(await this.Get(key));
|
||||
if (object === Object(object)) {
|
||||
return object;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
VRCXStorage.SetObject = function (key, value) {
|
||||
this.Set(key, JSON.stringify(value));
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
import {
|
||||
useAuthStore,
|
||||
useModalStore,
|
||||
useNotificationStore,
|
||||
useUpdateLoopStore,
|
||||
useUserStore
|
||||
} from '../stores';
|
||||
import { getCurrentUser } from '../coordinators/userCoordinator';
|
||||
import { AppDebug } from './appConfig.js';
|
||||
import { escapeTag } from '../shared/utils';
|
||||
import { i18n } from '../plugins/i18n';
|
||||
import { statusCodes } from '../shared/constants/api.js';
|
||||
import { watchState } from './watchState';
|
||||
|
||||
import webApiService from './webapi.js';
|
||||
|
||||
const pendingGetRequests = new Map();
|
||||
export let failedGetRequests = new Map();
|
||||
|
||||
const t = i18n.global.t;
|
||||
|
||||
/**
|
||||
* @param {string} endpoint
|
||||
* @param {object} [options]
|
||||
* @returns {object} init object ready for webApiService.execute
|
||||
*/
|
||||
export function buildRequestInit(endpoint, options) {
|
||||
const init = {
|
||||
url: `${AppDebug.endpointDomain}/${endpoint}`,
|
||||
method: 'GET',
|
||||
...options
|
||||
};
|
||||
const { params } = init;
|
||||
if (init.method === 'GET') {
|
||||
// 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();
|
||||
}
|
||||
} else if (
|
||||
init.uploadImage ||
|
||||
init.uploadFilePUT ||
|
||||
init.uploadImageLegacy
|
||||
) {
|
||||
// nothing — upload requests handle their own body
|
||||
} else {
|
||||
init.headers = {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
...init.headers
|
||||
};
|
||||
init.body = params === Object(params) ? JSON.stringify(params) : '{}';
|
||||
}
|
||||
return init;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a raw response: JSON-decodes response.data and detects API-level errors.
|
||||
* @param {{status: number, data?: string}} response
|
||||
* @returns {{status: number, data?: any, hasApiError?: boolean, parseError?: boolean}}
|
||||
*/
|
||||
export function parseResponse(response) {
|
||||
if (!response.data) {
|
||||
return response;
|
||||
}
|
||||
try {
|
||||
response.data = JSON.parse(response.data);
|
||||
if (response.data?.error) {
|
||||
return { ...response, hasApiError: true };
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
return { ...response, parseError: true };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {string} endpoint
|
||||
* @param {RequestInit & { params?: any } & {customMsg?: string}} [options]
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
export function request(endpoint, options) {
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const modalStore = useModalStore();
|
||||
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 = buildRequestInit(endpoint, 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
const parsed = parseResponse(response);
|
||||
if (AppDebug.debugWebRequests) {
|
||||
if (!parsed.data) {
|
||||
console.log(init, 'no data', parsed);
|
||||
} else {
|
||||
console.log(init, 'parsed data', parsed.data);
|
||||
}
|
||||
}
|
||||
if (parsed.hasApiError) {
|
||||
$throw(
|
||||
parsed.data.error.status_code || 0,
|
||||
parsed.data.error.message,
|
||||
endpoint
|
||||
);
|
||||
}
|
||||
if (parsed.parseError) {
|
||||
console.error('JSON parse error for', endpoint);
|
||||
if (parsed.status === 200) {
|
||||
$throw(
|
||||
0,
|
||||
t('api.error.message.invalid_json_response'),
|
||||
endpoint
|
||||
);
|
||||
}
|
||||
if (
|
||||
parsed.status === 429 &&
|
||||
init.url.endsWith('/instances/groups')
|
||||
) {
|
||||
updateLoopStore.setNextGroupInstanceRefresh(120); // 1min
|
||||
$throw(429, t('api.status_code.429'), endpoint);
|
||||
}
|
||||
if (parsed.status === 504 || parsed.status === 502) {
|
||||
// ignore expected API errors
|
||||
$throw(parsed.status, parsed.data || '', endpoint);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
})
|
||||
.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) {
|
||||
toast.success(
|
||||
options.customMsg ? options.customMsg : escapeTag(text)
|
||||
);
|
||||
}
|
||||
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) {
|
||||
getCurrentUser();
|
||||
}
|
||||
$throw(401, t('api.status_code.401'), endpoint);
|
||||
}
|
||||
}
|
||||
if (status === 403 && endpoint === 'config') {
|
||||
modalStore.alert({
|
||||
description: t('api.error.message.vpn_in_use'),
|
||||
title: `403 ${t('api.error.message.login_error')}`
|
||||
});
|
||||
authStore.handleLogoutEvent();
|
||||
$throw(403, endpoint);
|
||||
}
|
||||
if (
|
||||
init.method === 'GET' &&
|
||||
status === 404 &&
|
||||
endpoint?.startsWith('avatars/')
|
||||
) {
|
||||
$throw(404, data.error?.message || '', endpoint);
|
||||
}
|
||||
if (status === 404 && endpoint.endsWith('/persist/exists')) {
|
||||
return false;
|
||||
}
|
||||
if (status === 404 && endpoint.endsWith('/respond')) {
|
||||
// ignore when responding to expired notification
|
||||
return null;
|
||||
}
|
||||
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} [endpoint]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function shouldIgnoreError(code, endpoint) {
|
||||
if (
|
||||
(code === 404 || code === -1) &&
|
||||
typeof endpoint === 'string' &&
|
||||
endpoint.split('/').length === 2 &&
|
||||
(endpoint.startsWith('users/') ||
|
||||
endpoint.startsWith('worlds/') ||
|
||||
endpoint.startsWith('avatars/') ||
|
||||
endpoint.startsWith('groups/') ||
|
||||
endpoint.startsWith('file/'))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
(code === 403 || code === 404 || code === -1) &&
|
||||
endpoint?.startsWith('instances/')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (endpoint?.startsWith('analysis/')) {
|
||||
return true;
|
||||
}
|
||||
if (endpoint?.endsWith('/mutuals') && (code === 403 || code === -1)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 ignoreError = shouldIgnoreError(code, endpoint);
|
||||
if (
|
||||
(code === 403 || code === 404 || code === -1) &&
|
||||
endpoint?.includes('/mutuals/friends')
|
||||
) {
|
||||
message[1] = `${t('api.error.message.error_message')}: "${t('api.error.message.unavailable')}"`;
|
||||
}
|
||||
const text = message.map((s) => escapeTag(s)).join('\n');
|
||||
|
||||
if (text.length && !ignoreError) {
|
||||
toast.error(message[0], {
|
||||
description: message.slice(1).join('\n'),
|
||||
position: 'bottom-left'
|
||||
});
|
||||
}
|
||||
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] - Maximum number of items to fetch. -1 for unlimited, 0 for fetch until page size not met
|
||||
* @param {string} [options.limitParam] - 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);
|
||||
let batchSize = 0;
|
||||
if (Array.isArray(result.json)) {
|
||||
batchSize = result.json.length;
|
||||
} else if (Array.isArray(result.results)) {
|
||||
batchSize = result.results.length;
|
||||
} else {
|
||||
throw new Error(
|
||||
'Invalid result format: expected an array in result.json or result.results'
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof handle === 'function') {
|
||||
handle(result);
|
||||
}
|
||||
if (batchSize === 0) {
|
||||
break;
|
||||
}
|
||||
if (typeof result.hasNext === 'boolean' && !result.hasNext) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
const defaultAESKey = new TextEncoder().encode(
|
||||
'https://github.com/pypy-vrc/VRCX'
|
||||
);
|
||||
|
||||
const hexToUint8Array = (hexStr) => {
|
||||
const r = hexStr.match(/.{1,2}/g);
|
||||
if (!r) return null;
|
||||
return new Uint8Array(r.map((b) => parseInt(b, 16)));
|
||||
};
|
||||
|
||||
const uint8ArrayToHex = (arr) =>
|
||||
arr.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
function stdAESKey(key) {
|
||||
const tKey = new TextEncoder().encode(key);
|
||||
let sk = tKey;
|
||||
if (tKey.length < 32) {
|
||||
sk = new Uint8Array(32);
|
||||
sk.set(tKey);
|
||||
sk.set(defaultAESKey.slice(key.length, 32), key.length);
|
||||
}
|
||||
return sk.slice(0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param plaintext
|
||||
* @param key
|
||||
*/
|
||||
async function encrypt(plaintext, key) {
|
||||
let iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
let sharedKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
stdAESKey(key),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['encrypt']
|
||||
);
|
||||
let cipher = await window.crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
sharedKey,
|
||||
new TextEncoder().encode(plaintext)
|
||||
);
|
||||
let ciphertext = new Uint8Array(cipher);
|
||||
let encrypted = new Uint8Array(iv.length + ciphertext.byteLength);
|
||||
encrypted.set(iv, 0);
|
||||
encrypted.set(ciphertext, iv.length);
|
||||
return uint8ArrayToHex(encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ciphertext
|
||||
* @param key
|
||||
*/
|
||||
async function decrypt(ciphertext, key) {
|
||||
let text = hexToUint8Array(ciphertext);
|
||||
if (!text) return '';
|
||||
let sharedKey = await window.crypto.subtle.importKey(
|
||||
'raw',
|
||||
stdAESKey(key),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
true,
|
||||
['decrypt']
|
||||
);
|
||||
let plaintext = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: text.slice(0, 12) },
|
||||
sharedKey,
|
||||
text.slice(12)
|
||||
);
|
||||
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||
}
|
||||
|
||||
export default {
|
||||
decrypt,
|
||||
encrypt
|
||||
};
|
||||
|
||||
export { hexToUint8Array, uint8ArrayToHex, stdAESKey };
|
||||
@@ -0,0 +1,88 @@
|
||||
import { i18n } from '../plugins/i18n';
|
||||
import { openExternalLink } from '../shared/utils';
|
||||
import { useModalStore } from '../stores';
|
||||
|
||||
// requires binding of SQLite
|
||||
class SQLiteService {
|
||||
handleSQLiteError(e) {
|
||||
if (typeof e.message === 'string') {
|
||||
const modalStore = useModalStore();
|
||||
if (e.message.includes('database disk image is malformed')) {
|
||||
modalStore
|
||||
.confirm({
|
||||
description:
|
||||
'Please repair or delete your database file by following these instructions.',
|
||||
title: 'Your database is corrupted'
|
||||
})
|
||||
.then(({ ok }) => {
|
||||
if (!ok) return;
|
||||
openExternalLink(
|
||||
'https://github.com/vrcx-team/VRCX/wiki#how-to-repair-vrcx-database'
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
if (e.message.includes('database or disk is full')) {
|
||||
modalStore.alert({
|
||||
description: i18n.global.t('message.database.disk_space'),
|
||||
title: 'Disk containing database is full'
|
||||
});
|
||||
}
|
||||
if (
|
||||
e.message.includes('database is locked') ||
|
||||
e.message.includes('attempt to write a readonly database')
|
||||
) {
|
||||
modalStore.alert({
|
||||
description:
|
||||
'Please close other applications that might be using the database file.',
|
||||
title: 'Database is locked'
|
||||
});
|
||||
}
|
||||
if (e.message.includes('disk I/O error')) {
|
||||
modalStore.alert({
|
||||
description: i18n.global.t('message.database.disk_error'),
|
||||
title: 'Disk I/O error'
|
||||
});
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
async execute(callback, sql, args = null) {
|
||||
try {
|
||||
if (LINUX) {
|
||||
if (args) {
|
||||
args = new Map(Object.entries(args));
|
||||
}
|
||||
var json = await SQLite.ExecuteJson(sql, args);
|
||||
var items = JSON.parse(json);
|
||||
items.forEach((item) => {
|
||||
callback(item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var data = await SQLite.Execute(sql, args);
|
||||
data.forEach((row) => {
|
||||
callback(row);
|
||||
});
|
||||
} catch (e) {
|
||||
this.handleSQLiteError(e);
|
||||
}
|
||||
}
|
||||
|
||||
async executeNonQuery(sql, args = null) {
|
||||
try {
|
||||
if (LINUX && args) {
|
||||
args = new Map(Object.entries(args));
|
||||
}
|
||||
return await SQLite.ExecuteNonQuery(sql, args);
|
||||
} catch (e) {
|
||||
this.handleSQLiteError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var self = new SQLiteService();
|
||||
window.sqliteService = self;
|
||||
|
||||
export { self as default, SQLiteService };
|
||||
@@ -0,0 +1,8 @@
|
||||
import { reactive } from 'vue';
|
||||
const watchState = reactive({
|
||||
isLoggedIn: false,
|
||||
isFriendsLoaded: false,
|
||||
isFavoritesLoaded: false
|
||||
});
|
||||
|
||||
export { watchState };
|
||||
@@ -0,0 +1,51 @@
|
||||
// requires binding of WebApi
|
||||
|
||||
class WebApiService {
|
||||
clearCookies() {
|
||||
return WebApi.ClearCookies();
|
||||
}
|
||||
|
||||
getCookies() {
|
||||
return WebApi.GetCookies();
|
||||
}
|
||||
|
||||
setCookies(cookie) {
|
||||
return WebApi.SetCookies(cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} options
|
||||
* @returns {Promise<{status: number, data?: string}>}
|
||||
*/
|
||||
async execute(options) {
|
||||
if (!options) {
|
||||
throw new Error('options is required');
|
||||
}
|
||||
if (LINUX) {
|
||||
const requestJson = JSON.stringify(options);
|
||||
var json = await WebApi.ExecuteJson(requestJson);
|
||||
var data = JSON.parse(json);
|
||||
if (data.status === -1) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
return {
|
||||
status: data.status,
|
||||
data: data.message
|
||||
};
|
||||
}
|
||||
|
||||
var item = await WebApi.Execute(options);
|
||||
if (item.Item1 === -1) {
|
||||
throw item.Item2;
|
||||
}
|
||||
return {
|
||||
status: item.Item1,
|
||||
data: item.Item2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var self = new WebApiService();
|
||||
window.webApiService = self;
|
||||
|
||||
export { self as default, WebApiService };
|
||||
@@ -0,0 +1,580 @@
|
||||
import { reactive } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
import {
|
||||
useFriendStore,
|
||||
useGalleryStore,
|
||||
useGroupStore,
|
||||
useInstanceStore,
|
||||
useLocationStore,
|
||||
useNotificationStore,
|
||||
useSharedFeedStore,
|
||||
useUiStore,
|
||||
useUserStore
|
||||
} from '../stores';
|
||||
import { applyUser, applyCurrentUser } from '../coordinators/userCoordinator';
|
||||
import {
|
||||
onGroupLeft,
|
||||
applyGroup,
|
||||
getGroupDialogGroup,
|
||||
handleGroupMember
|
||||
} from '../coordinators/groupCoordinator';
|
||||
import {
|
||||
handleFriendAdd,
|
||||
handleFriendDelete
|
||||
} from '../coordinators/friendRelationshipCoordinator';
|
||||
import { escapeTag, parseLocation } from '../shared/utils';
|
||||
import { AppDebug } from './appConfig';
|
||||
import { groupRequest } from '../api';
|
||||
import { request } from './request';
|
||||
import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator';
|
||||
import { runSetCurrentUserLocationFlow } from '../coordinators/locationCoordinator';
|
||||
import { watchState } from './watchState';
|
||||
|
||||
import * as workerTimers from 'worker-timers';
|
||||
|
||||
let webSocket = null;
|
||||
let lastWebSocketMessage = '';
|
||||
|
||||
/**
|
||||
* Reactive WebSocket state for status bar telemetry.
|
||||
* - connected: whether the WS is currently open
|
||||
* - messageCount: total messages received (used for rate delta)
|
||||
*/
|
||||
export const wsState = reactive({
|
||||
connected: false,
|
||||
messageCount: 0,
|
||||
bytesReceived: 0
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function initWebsocket() {
|
||||
if (!watchState.isFriendsLoaded || webSocket !== null) {
|
||||
return;
|
||||
}
|
||||
return request('auth', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((json) => {
|
||||
const args = {
|
||||
json
|
||||
};
|
||||
if (args.json.ok) {
|
||||
connectWebSocket(args.json.token);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('WebSocket init error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
* @returns {void}
|
||||
*/
|
||||
function connectWebSocket(token) {
|
||||
const userStore = useUserStore();
|
||||
if (webSocket !== null) {
|
||||
return;
|
||||
}
|
||||
const socket = new WebSocket(`${AppDebug.websocketDomain}/?auth=${token}`);
|
||||
socket.onopen = () => {
|
||||
wsState.connected = true;
|
||||
if (AppDebug.debugWebSocket) {
|
||||
console.log('WebSocket connected');
|
||||
}
|
||||
};
|
||||
socket.onclose = () => {
|
||||
wsState.connected = false;
|
||||
if (webSocket === socket) {
|
||||
webSocket = null;
|
||||
}
|
||||
try {
|
||||
socket.close();
|
||||
} catch (err) {
|
||||
console.error('Error closing WebSocket:', err);
|
||||
}
|
||||
if (AppDebug.debugWebSocket) {
|
||||
console.log('WebSocket closed');
|
||||
}
|
||||
workerTimers.setTimeout(() => {
|
||||
if (
|
||||
watchState.isLoggedIn &&
|
||||
watchState.isFriendsLoaded &&
|
||||
webSocket === null
|
||||
) {
|
||||
initWebsocket();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
socket.onerror = () => {
|
||||
if (AppDebug.errorNoty) {
|
||||
toast.dismiss(AppDebug.errorNoty);
|
||||
}
|
||||
AppDebug.errorNoty = toast.error('WebSocket Error');
|
||||
socket.onclose(
|
||||
new CloseEvent('close', {
|
||||
code: 1006, // Abnormal Closure
|
||||
reason: 'WebSocket Error'
|
||||
})
|
||||
);
|
||||
};
|
||||
socket.onmessage = ({ data }) => {
|
||||
try {
|
||||
wsState.messageCount++;
|
||||
wsState.bytesReceived += data.length;
|
||||
if (lastWebSocketMessage === data) {
|
||||
// pls no spam
|
||||
return;
|
||||
}
|
||||
lastWebSocketMessage = data;
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(data);
|
||||
json.content = JSON.parse(json.content);
|
||||
} catch {
|
||||
// ignore parse error
|
||||
}
|
||||
handlePipeline({
|
||||
json
|
||||
});
|
||||
if (AppDebug.debugWebSocket && json.content) {
|
||||
let displayName = '';
|
||||
const user = userStore.cachedUsers.get(json.content.userId);
|
||||
if (user) {
|
||||
displayName = user.displayName;
|
||||
}
|
||||
console.log('WebSocket', json.type, displayName, json.content);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
webSocket = socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
export function closeWebSocket() {
|
||||
const socket = webSocket;
|
||||
if (socket === null) {
|
||||
return;
|
||||
}
|
||||
webSocket = null;
|
||||
try {
|
||||
socket.close();
|
||||
} catch (err) {
|
||||
console.error('Error closing WebSocket:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
export function reconnectWebSocket() {
|
||||
if (!watchState.isLoggedIn || !watchState.isFriendsLoaded) {
|
||||
return;
|
||||
}
|
||||
closeWebSocket();
|
||||
initWebsocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} args
|
||||
*/
|
||||
function handlePipeline(args) {
|
||||
const userStore = useUserStore();
|
||||
const locationStore = useLocationStore();
|
||||
const galleryStore = useGalleryStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const sharedFeedStore = useSharedFeedStore();
|
||||
const friendStore = useFriendStore();
|
||||
const groupStore = useGroupStore();
|
||||
const uiStore = useUiStore();
|
||||
const instanceStore = useInstanceStore();
|
||||
const { type, content, err } = args.json;
|
||||
if (typeof err !== 'undefined') {
|
||||
console.error('PIPELINE: error', args);
|
||||
if (AppDebug.errorNoty) {
|
||||
toast.dismiss(AppDebug.errorNoty);
|
||||
}
|
||||
AppDebug.errorNoty = toast.error(escapeTag(`WebSocket Error: ${err}`));
|
||||
return;
|
||||
}
|
||||
if (typeof content === 'undefined') {
|
||||
console.error('PIPELINE: missing content', args);
|
||||
return;
|
||||
}
|
||||
if (typeof content.user !== 'undefined') {
|
||||
// I forgot about this...
|
||||
delete content.user.state;
|
||||
}
|
||||
switch (type) {
|
||||
case 'notification':
|
||||
notificationStore.handleNotification({
|
||||
json: content,
|
||||
params: {
|
||||
notificationId: content.id
|
||||
}
|
||||
});
|
||||
notificationStore.handlePipelineNotification({
|
||||
json: content,
|
||||
params: {
|
||||
notificationId: content.id
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'notification-v2':
|
||||
console.log('notification-v2', content);
|
||||
notificationStore.handleNotificationV2({
|
||||
json: content,
|
||||
params: {
|
||||
notificationId: content.id
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'notification-v2-delete':
|
||||
console.log('notification-v2-delete', content);
|
||||
for (var id of content.ids) {
|
||||
notificationStore.handleNotificationV2Hide(id);
|
||||
notificationStore.handleNotificationSee(id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'notification-v2-update':
|
||||
console.log('notification-v2-update', content);
|
||||
notificationStore.handleNotificationV2Update({
|
||||
json: content.updates,
|
||||
params: {
|
||||
notificationId: content.id
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'see-notification':
|
||||
notificationStore.handleNotificationSee(content);
|
||||
break;
|
||||
|
||||
case 'hide-notification':
|
||||
notificationStore.handleNotificationHide(content);
|
||||
notificationStore.handleNotificationSee(content);
|
||||
break;
|
||||
|
||||
case 'response-notification':
|
||||
notificationStore.handleNotificationHide(content.notificationId);
|
||||
notificationStore.handleNotificationSee(content.notificationId);
|
||||
break;
|
||||
|
||||
case 'friend-add':
|
||||
applyUser(content.user);
|
||||
handleFriendAdd({
|
||||
params: {
|
||||
userId: content.userId
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'friend-delete':
|
||||
handleFriendDelete({
|
||||
params: {
|
||||
userId: content.userId
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'friend-online':
|
||||
// Where is instanceId, travelingToWorld, travelingToInstance?
|
||||
// More JANK, what a mess
|
||||
const $location = parseLocation(content.location);
|
||||
const $travelingToLocation = parseLocation(
|
||||
content.travelingToLocation
|
||||
);
|
||||
if (content?.user?.id) {
|
||||
const onlineJson = {
|
||||
id: content.userId,
|
||||
platform: content.platform,
|
||||
state: 'online',
|
||||
|
||||
location: content.location,
|
||||
worldId: content.worldId,
|
||||
instanceId: $location.instanceId,
|
||||
travelingToLocation: content.travelingToLocation,
|
||||
travelingToWorld: $travelingToLocation.worldId,
|
||||
travelingToInstance: $travelingToLocation.instanceId,
|
||||
|
||||
...content.user
|
||||
};
|
||||
applyUser(onlineJson);
|
||||
} else {
|
||||
console.error('friend-online missing user id', content);
|
||||
runUpdateFriendFlow(content.userId, 'online');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'friend-active':
|
||||
if (content?.user?.id) {
|
||||
const activeJson = {
|
||||
id: content.userId,
|
||||
platform: content.platform,
|
||||
state: 'active',
|
||||
|
||||
location: 'offline',
|
||||
worldId: 'offline',
|
||||
instanceId: 'offline',
|
||||
travelingToLocation: 'offline',
|
||||
travelingToWorld: 'offline',
|
||||
travelingToInstance: 'offline',
|
||||
|
||||
...content.user
|
||||
};
|
||||
applyUser(activeJson);
|
||||
} else {
|
||||
console.error('friend-active missing user id', content);
|
||||
runUpdateFriendFlow(content.userId, 'active');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'friend-offline':
|
||||
// more JANK, hell yeah
|
||||
const offlineJson = {
|
||||
id: content.userId,
|
||||
platform: content.platform,
|
||||
state: 'offline',
|
||||
|
||||
location: 'offline',
|
||||
worldId: 'offline',
|
||||
instanceId: 'offline',
|
||||
travelingToLocation: 'offline',
|
||||
travelingToWorld: 'offline',
|
||||
travelingToInstance: 'offline'
|
||||
};
|
||||
applyUser(offlineJson);
|
||||
break;
|
||||
|
||||
case 'friend-update':
|
||||
applyUser(content.user);
|
||||
break;
|
||||
|
||||
case 'friend-location':
|
||||
const $location1 = parseLocation(content.location);
|
||||
const $travelingToLocation1 = parseLocation(
|
||||
content.travelingToLocation
|
||||
);
|
||||
if (!content?.user?.id) {
|
||||
console.error('friend-location missing user id', content);
|
||||
const jankLocationJson = {
|
||||
id: content.userId,
|
||||
location: content.location,
|
||||
worldId: content.worldId,
|
||||
instanceId: $location1.instanceId,
|
||||
travelingToLocation: content.travelingToLocation,
|
||||
travelingToWorld: $travelingToLocation1.worldId,
|
||||
travelingToInstance: $travelingToLocation1.instanceId
|
||||
};
|
||||
applyUser(jankLocationJson);
|
||||
break;
|
||||
}
|
||||
const locationJson = {
|
||||
location: content.location,
|
||||
worldId: content.worldId,
|
||||
instanceId: $location1.instanceId,
|
||||
travelingToLocation: content.travelingToLocation,
|
||||
travelingToWorld: $travelingToLocation1.worldId,
|
||||
travelingToInstance: $travelingToLocation1.instanceId,
|
||||
...content.user,
|
||||
state: 'online' // JANK
|
||||
};
|
||||
applyUser(locationJson);
|
||||
|
||||
break;
|
||||
|
||||
case 'user-update':
|
||||
applyCurrentUser(content.user);
|
||||
break;
|
||||
|
||||
case 'user-location':
|
||||
// update current user location
|
||||
if (content.userId !== userStore.currentUser.id) {
|
||||
console.error('user-location wrong userId', content);
|
||||
break;
|
||||
}
|
||||
|
||||
// content.user: {} // we don't trust this
|
||||
// content.world: {} // this is long gone
|
||||
// content.worldId // where did worldId go?
|
||||
// content.instance // without worldId, this is useless
|
||||
|
||||
runSetCurrentUserLocationFlow(
|
||||
content.location,
|
||||
content.travelingToLocation
|
||||
);
|
||||
break;
|
||||
|
||||
case 'group-joined':
|
||||
// var groupId = content.groupId;
|
||||
// $app.onGroupJoined(groupId);
|
||||
break;
|
||||
|
||||
case 'group-left':
|
||||
onGroupLeft(content.groupId);
|
||||
break;
|
||||
|
||||
case 'group-role-updated':
|
||||
const groupId = content.role.groupId;
|
||||
groupRequest
|
||||
.getGroup({ groupId, includeRoles: true })
|
||||
.then((args) => applyGroup(args.json));
|
||||
console.log('group-role-updated', content);
|
||||
|
||||
// content {
|
||||
// role: {
|
||||
// createdAt: string,
|
||||
// description: string,
|
||||
// groupId: string,
|
||||
// id: string,
|
||||
// isManagementRole: boolean,
|
||||
// isSelfAssignable: boolean,
|
||||
// name: string,
|
||||
// order: number,
|
||||
// permissions: string[],
|
||||
// requiresPurchase: boolean,
|
||||
// requiresTwoFactor: boolean
|
||||
break;
|
||||
|
||||
case 'group-member-updated':
|
||||
var member = content.member;
|
||||
if (!member) {
|
||||
console.error('group-member-updated missing member', content);
|
||||
break;
|
||||
}
|
||||
const groupId1 = member.groupId;
|
||||
if (
|
||||
groupStore.groupDialog.visible &&
|
||||
groupStore.groupDialog.id === groupId1
|
||||
) {
|
||||
getGroupDialogGroup(groupId1);
|
||||
}
|
||||
handleGroupMember({
|
||||
json: member,
|
||||
params: {
|
||||
groupId: groupId1
|
||||
}
|
||||
});
|
||||
console.log('group-member-updated', member);
|
||||
break;
|
||||
|
||||
case 'instance-queue-joined':
|
||||
case 'instance-queue-position':
|
||||
var instanceId = content.instanceLocation;
|
||||
var position = content.position ?? 0;
|
||||
var queueSize = content.queueSize ?? 0;
|
||||
instanceStore.instanceQueueUpdate(instanceId, position, queueSize);
|
||||
break;
|
||||
|
||||
case 'instance-queue-ready':
|
||||
// var expiry = Date.parse(content.expiry);
|
||||
instanceStore.instanceQueueReady(content.instanceLocation);
|
||||
break;
|
||||
|
||||
case 'instance-queue-left':
|
||||
instanceStore.removeQueuedInstance(content.instanceLocation);
|
||||
// $app.instanceQueueClear();
|
||||
break;
|
||||
|
||||
case 'content-refresh':
|
||||
var contentType = content.contentType;
|
||||
console.log('content-refresh', content);
|
||||
if (contentType === 'icon') {
|
||||
if (
|
||||
galleryStore.galleryDialogVisible &&
|
||||
!galleryStore.galleryDialogIconsLoading
|
||||
) {
|
||||
galleryStore.refreshVRCPlusIconsTable();
|
||||
}
|
||||
} else if (contentType === 'gallery') {
|
||||
if (
|
||||
galleryStore.galleryDialogVisible &&
|
||||
!galleryStore.galleryDialogGalleryLoading
|
||||
) {
|
||||
galleryStore.refreshGalleryTable();
|
||||
}
|
||||
} else if (contentType === 'emoji') {
|
||||
if (
|
||||
galleryStore.galleryDialogVisible &&
|
||||
!galleryStore.galleryDialogEmojisLoading
|
||||
) {
|
||||
galleryStore.refreshEmojiTable();
|
||||
}
|
||||
} else if (contentType === 'sticker') {
|
||||
// on sticker upload
|
||||
} else if (contentType === 'print') {
|
||||
if (content.actionType === 'created') {
|
||||
galleryStore.tryDeleteOldPrints();
|
||||
} else if (
|
||||
galleryStore.galleryDialogVisible &&
|
||||
!galleryStore.galleryDialogPrintsLoading
|
||||
) {
|
||||
galleryStore.refreshPrintTable();
|
||||
}
|
||||
} else if (contentType === 'prints') {
|
||||
// lol
|
||||
} else if (contentType === 'avatar') {
|
||||
// hmm, utilizing this might be too spamy and cause UI to move around
|
||||
} else if (contentType === 'world') {
|
||||
// hmm
|
||||
} else if (contentType === 'created') {
|
||||
// on avatar upload, might be gone now
|
||||
} else if (contentType === 'avatargallery') {
|
||||
// on avatar gallery image upload
|
||||
} else if (contentType === 'invitePhoto') {
|
||||
// on uploading invite photo
|
||||
} else if (contentType === 'inventory') {
|
||||
if (
|
||||
galleryStore.galleryDialogVisible &&
|
||||
!galleryStore.galleryDialogInventoryLoading
|
||||
) {
|
||||
galleryStore.getInventory();
|
||||
}
|
||||
// on consuming a bundle
|
||||
// {contentType: 'inventory', itemId: 'inv_', itemType: 'prop', actionType: 'add'}
|
||||
} else if (!contentType) {
|
||||
console.log('content-refresh without contentType', content);
|
||||
} else {
|
||||
console.log(
|
||||
'Unknown content-refresh type',
|
||||
content.contentType
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'instance-closed':
|
||||
// TODO: get worldName, groupName, hardClose
|
||||
const noty = {
|
||||
type: 'instance.closed',
|
||||
location: content.instanceLocation,
|
||||
message: 'Instance Closed',
|
||||
created_at: new Date().toJSON()
|
||||
};
|
||||
if (
|
||||
notificationStore.notificationTable.filters[0].value.length ===
|
||||
0 ||
|
||||
notificationStore.notificationTable.filters[0].value.includes(
|
||||
noty.type
|
||||
)
|
||||
) {
|
||||
uiStore.notifyMenu('notification');
|
||||
}
|
||||
notificationStore.queueNotificationNoty(noty);
|
||||
notificationStore.appendNotificationTableEntry(noty);
|
||||
sharedFeedStore.addEntry(noty);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown pipeline type', args.json);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user