app: remove variables from store, reorganize logic

This commit is contained in:
Kmc
2024-06-27 20:37:41 -04:00
parent 42e799f3c9
commit 62e7555b1d
19 changed files with 405 additions and 310 deletions

View File

@@ -5,9 +5,10 @@
import PluginMenu from '$lib/Components/PluginMenu.svelte';
import LogView from '$lib/Components/LogView.svelte';
import { logger } from '$lib/Util/Logger';
import { config, showDisclaimer } from '$lib/Util/store';
import { showDisclaimer } from '$lib/Util/store';
import DisclaimerModal from '$lib/Components/DisclaimerModal.svelte';
import { BoltService } from '$lib/Services/BoltService';
import { config } from '$lib/State/Config';
let showPluginMenu: boolean = false;
let authorizing: boolean = false;

View File

@@ -1,14 +1,16 @@
<script lang="ts">
import { onMount } from 'svelte';
import { accountList, config, credentials, isConfigDirty, selectedPlay } from '$lib/Util/store';
import { accountList, selectedPlay } from '$lib/Util/store';
import { revokeOauthCreds } from '$lib/Util/functions';
import { logger } from '$lib/Util/Logger';
import { BoltService } from '$lib/Services/BoltService';
import { AuthService, type Credentials } from '$lib/Services/AuthService';
import { AuthService, type Session } from '$lib/Services/AuthService';
import { bolt } from '$lib/State/Bolt';
import { config } from '$lib/State/Config';
// values gather from s()
const sOrigin = BoltService.bolt.origin;
const clientId = BoltService.bolt.clientid;
const sOrigin = bolt.env.origin;
const clientId = bolt.env.clientid;
const exchangeUrl = sOrigin.concat('/oauth2/token');
const revokeUrl = sOrigin.concat('/oauth2/revoke');
@@ -21,7 +23,7 @@
return;
}
let creds: Credentials | undefined = $selectedPlay.credentials;
let creds: Session | undefined = $selectedPlay.credentials;
if ($selectedPlay.account) {
$accountList.delete($selectedPlay.account?.userId);
$accountList = $accountList; // certain data structure methods won't trigger an update, so we force one manually
@@ -47,7 +49,7 @@
revokeOauthCreds(creds!.access_token, revokeUrl, clientId).then((res: unknown) => {
if (res === 200) {
logger.info('Successful logout');
removeLogin(<Credentials>creds);
removeLogin(<Session>creds);
} else {
logger.error(`Logout unsuccessful: status ${res}`);
}
@@ -62,23 +64,22 @@
}
// clear credentials when logout is clicked
function removeLogin(creds: Credentials): void {
$credentials.delete(creds.sub);
BoltService.saveAllCreds();
function removeLogin(creds: Session): void {
const index = bolt.sessions.findIndex((session) => session.sub === creds.sub);
if (index > -1) {
bolt.sessions.splice(index, 1);
BoltService.saveAllCreds();
}
}
// updated active account in selected_play store
function accountChanged(): void {
isConfigDirty.set(true);
const key: string = <string>accountSelect[accountSelect.selectedIndex].getAttribute('data-id');
$selectedPlay.account = $accountList.get(key);
$config.selected_account = key;
$selectedPlay.credentials = $credentials.get(<string>$selectedPlay.account?.userId);
// Unsure what the equivalent of this is with the refector
//$selectedPlay.credentials = $credentials.get(<string>$selectedPlay.account?.userId);
// state updates can be weird, for some reason, changing the 'options' under the character_select
// does not trigger the 'on:change', so we force update it.
// I dislike this bit of code but couldn't think of another way to solve the problem other than props from the parent
if ($selectedPlay.account && $selectedPlay.account.characters) {
const char_select: HTMLSelectElement = <HTMLSelectElement>(
document.getElementById('character_select')
@@ -99,7 +100,8 @@
index++;
});
$selectedPlay.credentials = $credentials.get(<string>$selectedPlay.account?.userId);
// Unsure what the equivalent of this is with the refactor
//$selectedPlay.credentials = $credentials.get(<string>$selectedPlay.account?.userId);
});
</script>
@@ -120,7 +122,7 @@
<button
class="mx-auto mr-2 rounded-lg bg-blue-500 p-2 font-bold text-black duration-200 hover:opacity-75"
on:click={() => {
const { origin, redirect, clientid } = BoltService.bolt;
const { origin, redirect, clientid } = bolt.env;
AuthService.openLoginWindow(origin, redirect, clientid);
}}
>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import Modal from '$lib/Components/CommonUI/Modal.svelte';
import { credentials } from '$lib/Util/store';
import { bolt } from '$lib/State/Bolt';
import { onMount } from 'svelte';
let modal: Modal;
onMount(() => {
if ($credentials.size == 0) {
if (bolt.sessions.length == 0) {
modal.open();
}
});

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import { afterUpdate, onMount } from 'svelte';
import { get } from 'svelte/store';
import { launchHdos, launchRS3Linux, launchRuneLite } from '$lib/Util/functions';
import { Client, Game } from '$lib/Util/interfaces';
import { config, hasBoltPlugins, isConfigDirty, selectedPlay } from '$lib/Util/store';
import { selectedPlay } from '$lib/Util/store';
import { logger } from '$lib/Util/Logger';
import { bolt } from '$lib/State/Bolt';
import { config } from '$lib/State/Config';
export let showPluginMenu = false;
@@ -25,7 +26,6 @@
$selectedPlay.character?.accountId
);
}
$isConfigDirty = true;
}
// update selected_play
@@ -37,7 +37,6 @@
$selectedPlay.client = Client.hdos;
$config.selected_client_index = Client.hdos;
}
$isConfigDirty = true;
}
// when play is clicked, check the selected_play store for all relevant details
@@ -128,11 +127,11 @@
</select>
{:else if $selectedPlay.game == Game.rs3}
<button
disabled={!get(hasBoltPlugins)}
title={get(hasBoltPlugins) ? null : 'Coming soon...'}
disabled={!bolt.hasBoltPlugins}
title={bolt.hasBoltPlugins ? null : 'Coming soon...'}
class="mx-auto mb-2 w-52 rounded-lg p-2 font-bold text-black duration-200 enabled:bg-blue-500 enabled:hover:opacity-75 disabled:bg-gray-500"
on:click={() => {
showPluginMenu = get(hasBoltPlugins) ?? false;
showPluginMenu = bolt.hasBoltPlugins ?? false;
}}
>
Plugin menu

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { get } from 'svelte/store';
import { onDestroy } from 'svelte';
import { getNewClientListPromise, savePluginConfig } from '$lib/Util/functions';
import { type PluginConfig } from '$lib/Util/interfaces';
import { clientListPromise, hasBoltPlugins, pluginList, platform } from '$lib/Util/store';
import { clientListPromise } from '$lib/Util/store';
import { logger } from '$lib/Util/Logger';
import Modal from '$lib/Components/CommonUI/Modal.svelte';
import { bolt } from '$lib/State/Bolt';
// props
export let showPluginMenu: boolean;
@@ -29,7 +29,7 @@
};
const getPluginConfigPromiseFromID = (id: string): Promise<PluginConfig> | null => {
const list = get(pluginList);
const list = bolt.pluginList;
const meta = list[id];
if (!meta) return null;
const path = meta.path;
@@ -44,8 +44,8 @@
.then((plugin: PluginConfig) => {
do {
selectedPlugin = crypto.randomUUID();
} while (Object.keys(get(pluginList)).includes(selectedPlugin));
$pluginList[selectedPlugin] = {
} while (Object.keys(bolt.pluginList).includes(selectedPlugin));
bolt.pluginList[selectedPlugin] = {
name: plugin.name ?? unnamedPluginName,
path: folderPath
};
@@ -67,7 +67,7 @@
// if the user closes the file picker without selecting a file, status here is 204
if (xml.status == 200) {
const path: string =
get(platform) === 'windows' ? xml.responseText.replaceAll('\\', '/') : xml.responseText;
bolt.platform === 'windows' ? xml.responseText.replaceAll('\\', '/') : xml.responseText;
if (path.endsWith('/bolt.json')) {
const subpath: string = path.substring(0, path.length - 9);
handleNewPlugin(subpath, path);
@@ -176,12 +176,12 @@
{/await}
</div>
<div class="h-full pt-10">
{#if hasBoltPlugins}
{#if bolt.hasBoltPlugins}
<select
bind:value={selectedPlugin}
class="mx-auto mb-4 w-[min(280px,_45%)] cursor-pointer rounded-lg border-2 border-slate-300 bg-inherit p-2 text-inherit duration-200 hover:opacity-75 dark:border-slate-800"
>
{#each Object.entries($pluginList) as [id, plugin]}
{#each Object.entries(bolt.pluginList) as [id, plugin]}
<option class="dark:bg-slate-900" value={id}>{plugin.name ?? unnamedPluginName}</option>
{/each}
</select>
@@ -194,8 +194,8 @@
+
</button>
<br />
{#if Object.entries($pluginList).length !== 0}
{#if Object.keys(get(pluginList)).includes(selectedPlugin) && managementPluginPromise !== null}
{#if Object.entries(bolt.pluginList).length !== 0}
{#if Object.keys(bolt.pluginList).includes(selectedPlugin) && managementPluginPromise !== null}
{#await managementPluginPromise}
<p>loading...</p>
{:then plugin}
@@ -215,9 +215,9 @@
on:click={() => {
managementPluginPromise = null;
pluginConfigDirty = true;
let list = get(pluginList);
let list = bolt.pluginList;
delete list[selectedPlugin];
pluginList.set(list);
bolt.pluginList = list;
}}
>
Remove
@@ -241,15 +241,15 @@
{#await managementPluginPromise}
<p>loading...</p>
{:then plugin}
{#if plugin && plugin.main && Object.keys($pluginList).includes(selectedPlugin)}
{#if $pluginList[selectedPlugin].path}
{#if plugin && plugin.main && Object.keys(bolt.pluginList).includes(selectedPlugin)}
{#if bolt.pluginList[selectedPlugin].path}
<button
class="mx-auto mb-1 w-auto rounded-lg bg-emerald-500 p-2 font-bold text-black duration-200 hover:opacity-75"
on:click={() =>
startPlugin(
selectedClientId,
selectedPlugin,
$pluginList[selectedPlugin].path ?? '',
bolt.pluginList[selectedPlugin].path ?? '',
plugin.main ?? ''
)}
>

View File

@@ -1,8 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import { config, isConfigDirty, platform, selectedPlay } from '$lib/Util/store';
import { selectedPlay } from '$lib/Util/store';
import { launchRuneLiteConfigure } from '$lib/Util/functions';
import { logger } from '$lib/Util/Logger';
import { Platform, bolt } from '$lib/State/Bolt';
import { config } from '$lib/State/Config';
let customJarDiv: HTMLDivElement;
let customJarFile: HTMLTextAreaElement;
@@ -25,12 +27,10 @@
$config.runelite_custom_jar = '';
$config.runelite_use_custom_jar = false;
}
$isConfigDirty = true;
}
function textChanged(): void {
$config.runelite_custom_jar = customJarFile.value;
$isConfigDirty = true;
}
function selectFile(): void {
@@ -47,7 +47,6 @@
if (xml.status == 200) {
customJarFile.value = xml.responseText;
$config.runelite_custom_jar = xml.responseText;
$isConfigDirty = true;
}
customJarFileButton.disabled = false;
useJar.disabled = false;
@@ -83,7 +82,7 @@
$config.runelite_use_custom_jar = false;
}
if ($platform !== 'linux') {
if (bolt.platform !== Platform.Linux) {
flatpakDiv.remove();
}
});

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { bolt, config, isConfigDirty, hasBoltPlugins } from '$lib/Util/store';
import { bolt } from '$lib/State/Bolt';
import { config } from '$lib/State/Config';
let configUriDiv: HTMLDivElement;
let configUriAddress: HTMLTextAreaElement;
@@ -11,17 +12,15 @@
function toggleUriDiv(): void {
configUriDiv.classList.toggle('opacity-25');
configUriAddress.disabled = !configUriAddress.disabled;
$isConfigDirty = true;
if (!useUri.checked) {
configUriAddress.value = atob($bolt.default_config_uri);
configUriAddress.value = atob(bolt.env.default_config_uri);
$config.rs_config_uri = '';
}
}
function uriAddressChanged(): void {
$config.rs_config_uri = configUriAddress.value;
$isConfigDirty = true;
}
// loads configs for menu
@@ -31,13 +30,13 @@
useUri.checked = true;
toggleUriDiv();
} else {
configUriAddress.value = atob($bolt.default_config_uri);
configUriAddress.value = atob(bolt.env.default_config_uri);
}
});
</script>
<div id="rs3_options" class="col-span-3 p-5 pt-10">
{#if hasBoltPlugins}
{#if bolt.hasBoltPlugins}
<div class="mx-auto p-2">
<label for="enable_plugins">Enable Bolt plugin loader: </label>
<input
@@ -45,7 +44,6 @@
name="enable_plugins"
id="enable_plugins"
bind:checked={$config.rs_plugin_loader}
on:change={() => isConfigDirty.set(true)}
class="ml-2"
/>
</div>

View File

@@ -1,12 +1,13 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Game } from '$lib/Util/interfaces';
import { config, isConfigDirty, selectedPlay } from '$lib/Util/store';
import { selectedPlay } from '$lib/Util/store';
import SettingsModal from '$lib/Components/SettingsModal.svelte';
import Dropdown from '$lib/Components/CommonUI/Dropdown.svelte';
import Account from '$lib/Components/Account.svelte';
import { BoltService } from '$lib/Services/BoltService';
import { AuthService } from '$lib/Services/AuthService';
import { bolt } from '$lib/State/Bolt';
import { config } from '$lib/State/Config';
let settingsModal: SettingsModal;
let rs3Button: HTMLButtonElement;
@@ -19,14 +20,12 @@
$selectedPlay.game = Game.osrs;
$selectedPlay.client = $config.selected_client_index;
$config.selected_game_index = Game.osrs;
$isConfigDirty = true;
osrsButton.classList.add('bg-blue-500', 'text-black');
rs3Button.classList.remove('bg-blue-500', 'text-black');
break;
case Game.rs3:
$selectedPlay.game = Game.rs3;
$config.selected_game_index = Game.rs3;
$isConfigDirty = true;
osrsButton.classList.remove('bg-blue-500', 'text-black');
rs3Button.classList.add('bg-blue-500', 'text-black');
break;
@@ -92,7 +91,7 @@
<button
class="h-11 w-48 rounded-lg border-2 border-slate-300 bg-inherit p-2 text-center font-bold text-black duration-200 hover:opacity-75 dark:border-slate-800 dark:text-slate-50"
on:click={() => {
const { origin, redirect, clientid } = BoltService.bolt;
const { origin, redirect, clientid } = bolt.env;
AuthService.openLoginWindow(origin, redirect, clientid);
}}
>

View File

@@ -0,0 +1,57 @@
const BASE_URL = 'bolt-internal';
export class ApiService {
static async get(route: string, params?: URLSearchParams): Promise<Response> {
let parsedParams = '';
if (params instanceof URLSearchParams) {
parsedParams += '?' + params.toString();
}
const url = `${BASE_URL}/${route + parsedParams}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
return response;
}
static async post(route: string, params: unknown): Promise<Response> {
const url = `${BASE_URL}/${route}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
});
return response;
}
static async put(route: string, body: unknown): Promise<Response> {
const url = `${BASE_URL}/${route}`;
const response = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
return response;
}
static async delete(route: string): Promise<Response> {
const url = `${BASE_URL}/${route}`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
return response;
}
}

View File

@@ -1,10 +1,9 @@
import { BoltService } from '$lib/Services/BoltService';
import { bolt } from '$lib/State/Bolt';
import { ParseUtils } from '$lib/Util/ParseUtils';
import { StringUtils } from '$lib/Util/StringUtils';
import { unwrap, type Account } from '$lib/Util/interfaces';
// credential type, passed around often
export interface Credentials {
export interface Session {
access_token: string;
id_token: string;
refresh_token: string;
@@ -23,7 +22,7 @@ interface PendingLogin {
export interface Auth {
state?: string;
nonce?: string;
creds?: Credentials;
creds?: Session;
win?: Window | null;
account_info_promise?: Promise<Account>;
verifier?: string;
@@ -59,9 +58,9 @@ export class AuthService {
// makes a request to the account_info endpoint and returns the promise
// the promise will return either a JSON object on success or a status code on failure
static getStandardAccountInfo(creds: Credentials): Promise<Account | number> {
static getStandardAccountInfo(creds: Session): Promise<Account | number> {
return new Promise((resolve) => {
const url = `${BoltService.bolt.api}/users/${creds.sub}/displayName`;
const url = `${bolt.env.api}/users/${creds.sub}/displayName`;
const xml = new XMLHttpRequest();
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
@@ -82,7 +81,7 @@ export class AuthService {
// and renews them using the oauth endpoint if so.
// Does not save credentials but sets credentials_are_dirty as appropriate.
// Returns null on success or an http status code on failure
static async checkRenewCreds(creds: Credentials, url: string, clientId: string) {
static async checkRenewCreds(creds: Session, url: string, clientId: string) {
return new Promise((resolve) => {
// only renew if less than 30 seconds left
if (creds.expiry - Date.now() < 30000) {

View File

@@ -1,44 +1,37 @@
import type { Credentials } from '$lib/Services/AuthService';
import { configHasPendingChanges, type Config } from '$lib/State/Config';
import { bolt } from '$lib/State/Bolt';
import { logger } from '$lib/Util/Logger';
import type { Bolt, Config } from '$lib/Util/interfaces';
import { credentials, isConfigDirty } from '$lib/Util/store';
import { get } from 'svelte/store';
let saveConfigInProgress: boolean = false;
let saveInProgress: boolean = false;
export class BoltService {
static bolt: Bolt;
// sends an asynchronous request to save the current user config to disk, if it has changed
static saveConfig(configToSave: Config) {
if (get(isConfigDirty) && !saveConfigInProgress) {
saveConfigInProgress = true;
const xml = new XMLHttpRequest();
xml.open('POST', '/save-config', true);
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
logger.info(`Save config status: '${xml.responseText.trim()}'`);
if (xml.status == 200) {
isConfigDirty.set(false);
}
saveConfigInProgress = false;
}
};
xml.setRequestHeader('Content-Type', 'application/json');
if (!configHasPendingChanges || saveInProgress) return;
// converting map into something that is compatible for JSON.stringify
// maybe the map should be converted into a Record<string, string>
// but this has other problems and implications
const characters: Record<string, string> = {};
configToSave.selected_characters?.forEach((value, key) => {
characters[key] = value;
});
const object: Record<string, unknown> = {};
Object.assign(object, configToSave);
object.selected_characters = characters;
const json = JSON.stringify(object, null, 4);
xml.send(json);
}
saveInProgress = true;
const xml = new XMLHttpRequest();
xml.open('POST', '/save-config', true);
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
logger.info(`Save config status: '${xml.responseText.trim()}'`);
saveInProgress = false;
}
};
xml.setRequestHeader('Content-Type', 'application/json');
// converting map into something that is compatible for JSON.stringify
// maybe the map should be converted into a Record<string, string>
// but this has other problems and implications
const characters: Record<string, string> = {};
configToSave.selected_characters?.forEach((value, key) => {
characters[key] = value;
});
const object: Record<string, unknown> = {};
Object.assign(object, configToSave);
object.selected_characters = characters;
const json = JSON.stringify(object, null, 4);
xml.send(json);
}
// sends a request to save all credentials to their config file,
@@ -58,11 +51,8 @@ export class BoltService {
// data.credentials = credentialsSub.get(<string>selectedPlaySub.account?.userId);
// return data;
// });
console.log('saveAllCreds', bolt.sessions);
const credsList: Array<Credentials> = [];
get(credentials).forEach((value) => {
credsList.push(value);
});
xml.send(JSON.stringify(credsList));
xml.send(JSON.stringify(bolt.sessions));
}
}

50
app/src/lib/State/Bolt.ts Normal file
View File

@@ -0,0 +1,50 @@
import type { Session } from '$lib/Services/AuthService';
import { ParseUtils } from '$lib/Util/ParseUtils';
import type { PluginMeta } from '$lib/Util/interfaces';
export const enum Platform {
Windows = 'windows',
Linux = 'linux',
MacOS = 'mac'
}
export interface BoltEnv {
provider: string;
origin: string;
origin_2fa: string;
redirect: string;
clientid: string;
api: string;
auth_api: string;
profile_api: string;
shield_url: string;
content_url: string;
default_config_uri: string;
games: string[];
}
export interface Bolt {
env: BoltEnv;
platform: Platform | null;
rs3InstalledHash: string | null;
runeLiteInstalledId: string | null;
hdosInstalledVersion: string | null;
isFlathub: boolean;
hasBoltPlugins: boolean;
pluginList: { [key: string]: PluginMeta }; // May need to be a writable
sessions: Session[];
}
declare const s: () => BoltEnv;
export const bolt: Bolt = {
env: ParseUtils.decodeBolt(s()),
platform: null,
rs3InstalledHash: null,
runeLiteInstalledId: null,
hdosInstalledVersion: null,
isFlathub: false,
hasBoltPlugins: false,
pluginList: {},
sessions: []
};

View File

@@ -0,0 +1,46 @@
import { logger } from '$lib/Util/Logger';
import { onWritableChange } from '$lib/Util/onWritableChange';
import { writable } from 'svelte/store';
export interface Config {
use_dark_theme: boolean;
rs_plugin_loader: boolean;
flatpak_rich_presence: boolean;
runelite_use_custom_jar: boolean;
selected_game_index: number;
selected_client_index: number;
rs_config_uri?: string;
runelite_custom_jar?: string;
selected_account?: string;
selected_characters?: Map<string, string>; // account userId, then character accountId
selected_game_accounts?: Map<string, string>; // legacy version of selected_characters
}
export let configHasPendingChanges = false;
const params = new URLSearchParams(window.location.search);
const configParam = params.get('config');
let parsedConfig: Config = {
use_dark_theme: true,
rs_plugin_loader: false,
flatpak_rich_presence: false,
runelite_use_custom_jar: false,
selected_characters: new Map(),
selected_game_accounts: new Map(),
selected_game_index: 1,
selected_client_index: 1
};
if (configParam) {
try {
parsedConfig = JSON.parse(configParam) as Config;
} catch (e) {
logger.error('Unable to parse config, restoring to default');
}
}
export const config = writable<Config>(parsedConfig);
onWritableChange(config, () => {
configHasPendingChanges = true;
});

View File

@@ -1,21 +1,12 @@
import type { Credentials } from '$lib/Services/AuthService';
import type { Session } from '$lib/Services/AuthService';
import type { BoltEnv } from '$lib/State/Bolt';
import { logger } from '$lib/Util/Logger';
import type { Bolt, Result } from '$lib/Util/interfaces';
import {
config,
credentials,
hasBoltPlugins,
hdosInstalledVersion,
platform,
pluginList,
rs3InstalledHash,
runeLiteInstalledId
} from '$lib/Util/store';
import type { Result } from '$lib/Util/interfaces';
export class ParseUtils {
// parses a response from the oauth endpoint
// returns a Result, it may
static parseCredentials(str: string): Result<Credentials> {
static parseCredentials(str: string): Result<Session> {
const oauthCreds = JSON.parse(str);
const sections = oauthCreds.id_token.split('.');
if (sections.length !== 3) {
@@ -44,64 +35,64 @@ export class ParseUtils {
};
}
static parseUrlParams(url: string) {
const query = new URLSearchParams(url);
platform.set(query.get('platform'));
//isFlathub = query.get('flathub') === '1';
rs3InstalledHash.set(query.get('rs3_linux_installed_hash'));
runeLiteInstalledId.set(query.get('runelite_installed_id'));
hdosInstalledVersion.set(query.get('hdos_installed_version'));
const queryPlugins: string | null = query.get('plugins');
if (queryPlugins !== null) {
hasBoltPlugins.set(true);
pluginList.set(JSON.parse(queryPlugins));
} else {
hasBoltPlugins.set(false);
}
// static parseUrlParams(url: string) {
// const query = new URLSearchParams(url);
// platform.set(query.get('platform'));
// //isFlathub = query.get('flathub') === '1';
// rs3InstalledHash.set(query.get('rs3_linux_installed_hash'));
// runeLiteInstalledId.set(query.get('runelite_installed_id'));
// hdosInstalledVersion.set(query.get('hdos_installed_version'));
// const queryPlugins: string | null = query.get('plugins');
// if (queryPlugins !== null) {
// hasBoltPlugins.set(true);
// pluginList.set(JSON.parse(queryPlugins));
// } else {
// hasBoltPlugins.set(false);
// }
const creds = query.get('credentials');
if (creds) {
try {
// no need to set credentials_are_dirty here because the contents came directly from the file
const credsList: Array<Credentials> = JSON.parse(creds);
credsList.forEach((value) => {
credentials.update((data) => {
data.set(value.sub, value);
return data;
});
});
} catch (error: unknown) {
logger.error(`Couldn't parse credentials file: ${error}`);
}
}
const conf = query.get('config');
if (conf) {
try {
// as above, no need to set configIsDirty
const parsedConf = JSON.parse(conf);
config.set(parsedConf);
// convert parsed objects into Maps
config.update((data) => {
if (data.selected_game_accounts) {
data.selected_characters = new Map(Object.entries(data.selected_game_accounts));
delete data.selected_game_accounts;
} else if (data.selected_characters) {
data.selected_characters = new Map(Object.entries(data.selected_characters));
}
return data;
});
} catch (error: unknown) {
logger.error(`Couldn't parse config file: ${error}`);
}
}
}
// const creds = query.get('credentials');
// if (creds) {
// try {
// // no need to set credentials_are_dirty here because the contents came directly from the file
// const credsList: Array<Session> = JSON.parse(creds);
// credsList.forEach((value) => {
// credentials.update((data) => {
// data.set(value.sub, value);
// return data;
// });
// });
// } catch (error: unknown) {
// logger.error(`Couldn't parse credentials file: ${error}`);
// }
// }
// const conf = query.get('config');
// if (conf) {
// try {
// // as above, no need to set configIsDirty
// const parsedConf = JSON.parse(conf);
// config.set(parsedConf);
// // convert parsed objects into Maps
// config.update((data) => {
// if (data.selected_game_accounts) {
// data.selected_characters = new Map(Object.entries(data.selected_game_accounts));
// delete data.selected_game_accounts;
// } else if (data.selected_characters) {
// data.selected_characters = new Map(Object.entries(data.selected_characters));
// }
// return data;
// });
// } catch (error: unknown) {
// logger.error(`Couldn't parse config file: ${error}`);
// }
// }
// }
// The bolt object from the C++ app has its values base64 encoded.
// This will decode each value, and return the original structure
static decodeBolt(encoded: Bolt): Bolt {
static decodeBolt(encoded: BoltEnv): BoltEnv {
const decodedObject: Record<string, string | string[]> = {};
for (const _key in encoded) {
const key = _key as keyof Bolt;
const key = _key as keyof BoltEnv;
const value = encoded[key];
if (typeof value === 'string') {
decodedObject[key] = atob(value);
@@ -112,6 +103,6 @@ export class ParseUtils {
}
}
return decodedObject as unknown as Bolt;
return decodedObject as unknown as BoltEnv;
}
}

View File

@@ -1,21 +1,12 @@
import { get } from 'svelte/store';
import { type Account, type Character, type GameClient } from '$lib/Util/interfaces';
import {
accountList,
config,
credentials,
pluginList,
hdosInstalledVersion,
internalUrl,
productionClientId,
rs3InstalledHash,
runeLiteInstalledId,
selectedPlay
} from '$lib/Util/store';
import { accountList, internalUrl, productionClientId, selectedPlay } from '$lib/Util/store';
import { logger } from '$lib/Util/Logger';
import { BoltService } from '$lib/Services/BoltService';
import { AuthService, type Credentials } from '$lib/Services/AuthService';
import { AuthService, type Session } from '$lib/Services/AuthService';
import { StringUtils } from '$lib/Util/StringUtils';
import { bolt } from '$lib/State/Bolt';
import { config } from '$lib/State/Config';
// deprecated?
// const rs3_basic_auth = 'Basic Y29tX2phZ2V4X2F1dGhfZGVza3RvcF9yczpwdWJsaWM=';
@@ -25,7 +16,7 @@ import { StringUtils } from '$lib/Util/StringUtils';
// Handles a new session id as part of the login flow. Can also be called on startup with a
// persisted session id.
export async function handleNewSessionId(
creds: Credentials,
creds: Session,
accountsUrl: string,
accountsInfoPromise: Promise<Account>
) {
@@ -74,10 +65,10 @@ export async function handleNewSessionId(
// called on new successful login with credentials. Delegates to a specific handler based on login_provider value.
// however it does not save credentials. You should call saveAllCreds after calling this function any number of times.
// Returns true if the credentials should be treated as valid by the caller immediately after return, or false if not.
export async function handleLogin(win: Window | null, creds: Credentials) {
export async function handleLogin(win: Window | null, creds: Session) {
const state = StringUtils.makeRandomState();
const nonce: string = crypto.randomUUID();
const location = BoltService.bolt.origin.concat('/oauth2/auth?').concat(
const location = bolt.env.origin.concat('/oauth2/auth?').concat(
new URLSearchParams({
id_token_hint: creds.id_token,
nonce: btoa(nonce),
@@ -109,7 +100,7 @@ export async function handleLogin(win: Window | null, creds: Credentials) {
return await handleNewSessionId(
creds,
BoltService.bolt.auth_api.concat('/accounts'),
bolt.env.auth_api.concat('/accounts'),
<Promise<Account>>accountInfoPromise
);
}
@@ -122,7 +113,9 @@ export function addNewAccount(account: Account) {
data.account = account;
const [firstKey] = account.characters.keys();
data.character = account.characters.get(firstKey);
if (get(credentials).size > 0) data.credentials = get(credentials).get(account.userId);
if (bolt.sessions.length > 0) {
data.credentials = bolt.sessions.find((session) => session.sub === account.userId);
}
return data;
});
};
@@ -132,6 +125,7 @@ export function addNewAccount(account: Account) {
return data;
});
const { config } = config;
if (get(selectedPlay).account && get(config).selected_account) {
if (account.userId == get(config).selected_account) {
updateSelectedPlay();
@@ -164,13 +158,14 @@ export function launchRS3Linux(
jx_character_id: string,
jx_display_name: string
) {
const { config } = config;
BoltService.saveConfig(get(config));
const launch = (hash?: unknown, deb?: never) => {
const launch = (hash?: string, deb?: never) => {
const xml = new XMLHttpRequest();
const params: Record<string, string> = {};
const _config = get(config);
if (hash) params.hash = <string>hash;
if (hash) params.hash = hash;
if (jx_session_id) params.jx_session_id = jx_session_id;
if (jx_character_id) params.jx_character_id = jx_character_id;
if (jx_display_name) params.jx_display_name = jx_display_name;
@@ -178,14 +173,14 @@ export function launchRS3Linux(
if (_config.rs_config_uri) {
params.config_uri = _config.rs_config_uri;
} else {
params.config_uri = BoltService.bolt.default_config_uri;
params.config_uri = bolt.env.default_config_uri;
}
xml.open('POST', '/launch-rs3-deb?'.concat(new URLSearchParams(params).toString()), true);
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
logger.info(`Game launch status: '${xml.responseText.trim()}'`);
if (xml.status == 200 && hash) {
rs3InstalledHash.set(<string>hash);
bolt.rs3InstalledHash = hash;
}
}
};
@@ -193,7 +188,7 @@ export function launchRS3Linux(
};
const xml = new XMLHttpRequest();
const contentUrl = BoltService.bolt.content_url;
const contentUrl = bolt.env.content_url;
const url = contentUrl.concat('dists/trusty/non-free/binary-amd64/Packages');
xml.open('GET', url, true);
xml.onreadystatechange = () => {
@@ -208,7 +203,7 @@ export function launchRS3Linux(
launch();
return;
}
if (lines.SHA256 !== get(rs3InstalledHash)) {
if (lines.SHA256 !== bolt.rs3InstalledHash) {
logger.info('Downloading RS3 client...');
const exeXml = new XMLHttpRequest();
exeXml.open('GET', contentUrl.concat(lines.Filename), true);
@@ -251,10 +246,11 @@ function launchRuneLiteInner(
jx_display_name: string,
configure: boolean
) {
const { config } = config;
BoltService.saveConfig(get(config));
const launchPath = configure ? '/launch-runelite-jar-configure?' : '/launch-runelite-jar?';
const launch = (id?: unknown, jar?: unknown, jarPath?: unknown) => {
const launch = (id?: string | null, jar?: unknown, jarPath?: unknown) => {
const xml = new XMLHttpRequest();
const params: Record<string, string> = {};
if (id) params.id = <string>id;
@@ -268,7 +264,7 @@ function launchRuneLiteInner(
if (xml.readyState == 4) {
logger.info(`Game launch status: '${xml.responseText.trim()}'`);
if (xml.status == 200 && id) {
runeLiteInstalledId.set(<string>id);
bolt.runeLiteInstalledId = id;
}
}
};
@@ -290,7 +286,7 @@ function launchRuneLiteInner(
.map((x: Record<string, string>) => x.assets)
.flat()
.find((x: Record<string, string>) => x.name.toLowerCase() == 'runelite.jar');
if (runelite.id != get(runeLiteInstalledId)) {
if (runelite.id != bolt.runeLiteInstalledId) {
logger.info('Downloading RuneLite...');
const xmlRl = new XMLHttpRequest();
xmlRl.open('GET', runelite.browser_download_url, true);
@@ -347,6 +343,7 @@ export function launchHdos(
jx_character_id: string,
jx_display_name: string
) {
const { config } = config;
BoltService.saveConfig(get(config));
const launch = (version?: string, jar?: string) => {
@@ -361,7 +358,7 @@ export function launchHdos(
if (xml.readyState == 4) {
logger.info(`Game launch status: '${xml.responseText.trim()}'`);
if (xml.status == 200 && version) {
hdosInstalledVersion.set(version);
bolt.hdosInstalledVersion = version;
}
}
};
@@ -379,7 +376,7 @@ export function launchHdos(
);
if (versionRegex && versionRegex.length >= 2) {
const latestVersion = versionRegex[1];
if (latestVersion !== get(hdosInstalledVersion)) {
if (latestVersion !== bolt.hdosInstalledVersion) {
const jarUrl = `https://cdn.hdos.dev/launcher/v${latestVersion}/hdos-launcher.jar`;
logger.info('Downloading HDOS...');
const xmlHdos = new XMLHttpRequest();
@@ -455,5 +452,5 @@ export function savePluginConfig(): void {
logger.info(`Save-plugin-config status: ${xml.responseText.trim()}`);
}
};
xml.send(JSON.stringify(get(pluginList)));
xml.send(JSON.stringify(bolt.pluginList));
}

View File

@@ -1,6 +1,6 @@
// file for all interfaces, types, and their helper functions
import type { Credentials } from '$lib/Services/AuthService';
import type { Session } from '$lib/Services/AuthService';
// result type, similar to rust's implementation
// useful if a function may succeed or fail
@@ -20,6 +20,14 @@ export function unwrap<T, E = Error>(result: Result<T, E>): T {
}
}
export function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
export function error<T>(error: T): Result<never, T> {
return { ok: false, error };
}
// game enum
export enum Game {
rs3,
@@ -33,51 +41,6 @@ export enum Client {
rs3
}
// s()
export interface Bolt {
provider: string;
origin: string;
origin_2fa: string;
redirect: string;
clientid: string;
api: string;
auth_api: string;
profile_api: string;
shield_url: string;
content_url: string;
default_config_uri: string;
games: string[];
}
// load on start and save on exit
export interface Config {
use_dark_theme?: boolean;
rs_plugin_loader?: boolean;
rs_config_uri?: string;
flatpak_rich_presence?: boolean;
runelite_use_custom_jar?: boolean;
runelite_custom_jar?: string;
selected_account?: string;
selected_characters?: Map<string, string>; // account userId, then character accountId
selected_game_accounts?: Map<string, string>; // legacy version of selected_characters
selected_game_index?: number;
selected_client_index?: number;
}
// if no config is loaded, these defaults are set to ensure the app runs
export const defaultConfig: Config = {
use_dark_theme: true,
flatpak_rich_presence: false,
rs_config_uri: '',
runelite_custom_jar: '',
runelite_use_custom_jar: false,
selected_account: '',
selected_characters: new Map(),
selected_game_accounts: new Map(),
selected_game_index: 1,
selected_client_index: 1
};
// account info
export interface Account {
id: string;
@@ -99,7 +62,7 @@ export interface Character {
export interface SelectedPlay {
account?: Account;
character?: Character;
credentials?: Credentials;
credentials?: Session;
game?: Game;
client?: Client;
}

View File

@@ -0,0 +1,12 @@
import type { Writable } from 'svelte/store';
export function onWritableChange<T>(store: Writable<T>, fn: (newValue: T) => void) {
let initialized = false;
return store.subscribe((value) => {
if (initialized) {
fn(value);
} else {
initialized = true;
}
});
}

View File

@@ -1,16 +1,11 @@
import { readable, writable, type Readable, type Writable } from 'svelte/store';
import {
type Config,
type Bolt,
type Account,
type SelectedPlay,
type GameClient,
type PluginMeta,
defaultConfig,
Game,
Client
} from '$lib/Util/interfaces';
import type { Credentials } from '$lib/Services/AuthService';
// readable stores. known at starup
export const internalUrl: Readable<string> = readable('https://bolt-internal');
@@ -18,18 +13,7 @@ export const productionClientId: Readable<string> = readable(
'1fddee4e-b100-4f4e-b2b0-097f9088f9d2'
);
// writable stores
export const bolt: Writable<Bolt> = writable();
export const platform: Writable<string | null> = writable('');
export const config: Writable<Config> = writable(defaultConfig);
export const credentials: Writable<Map<string, Credentials>> = writable(new Map());
export const hasBoltPlugins: Writable<boolean> = writable(false);
export const pluginList: Writable<{ [key: string]: PluginMeta }> = writable();
export const clientListPromise: Writable<Promise<GameClient[]>> = writable();
export const rs3InstalledHash: Writable<string | null> = writable('');
export const runeLiteInstalledId: Writable<string | null> = writable('');
export const hdosInstalledVersion: Writable<string | null> = writable('');
export const isConfigDirty: Writable<boolean> = writable(false);
export const accountList: Writable<Map<string, Account>> = writable(new Map());
export const selectedPlay: Writable<SelectedPlay> = writable({
game: Game.osrs,

View File

@@ -1,41 +1,62 @@
import App from '@/App.svelte';
import {
clientListPromise,
internalUrl,
credentials,
isConfigDirty,
showDisclaimer
} from '$lib/Util/store';
import { clientListPromise, internalUrl, showDisclaimer } from '$lib/Util/store';
import { get } from 'svelte/store';
import { getNewClientListPromise, handleLogin, handleNewSessionId } from '$lib/Util/functions';
import type { Account, Bolt } from '$lib/Util/interfaces';
import type { Account } from '$lib/Util/interfaces';
import { unwrap } from '$lib/Util/interfaces';
import { logger } from '$lib/Util/Logger';
import { BoltService } from '$lib/Services/BoltService';
import { ParseUtils } from '$lib/Util/ParseUtils';
import { AuthService, type Auth, type Credentials } from '$lib/Services/AuthService';
import { AuthService, type Auth, type Session } from '$lib/Services/AuthService';
import { Platform, bolt } from '$lib/State/Bolt';
initBolt();
addWindowListeners();
declare const s: () => Bolt;
BoltService.bolt = ParseUtils.decodeBolt(s());
start();
const app = new App({
target: document.getElementById('app')!
});
export default app;
export function start(): void {
// TODO: refactor this function and its contents
ParseUtils.parseUrlParams(window.location.search);
function initBolt() {
const params = new URLSearchParams(window.location.search);
bolt.platform = params.get('platform') as Platform | null;
bolt.isFlathub = params.get('flathub') === '1';
bolt.rs3InstalledHash = params.get('rs3_linux_installed_hash');
bolt.runeLiteInstalledId = params.get('runelite_installed_id');
bolt.hdosInstalledVersion = params.get('hdos_installed_version');
const bolt = BoltService.bolt;
const origin = bolt.origin;
const clientId = bolt.clientid;
const origin_2fa = bolt.origin_2fa;
const plugins = params.get('plugins');
bolt.hasBoltPlugins = plugins !== null;
if (plugins !== null) {
try {
bolt.pluginList = JSON.parse(plugins);
} catch (e) {
logger.error('Unable to parse plugin list');
}
}
const sessionsParam = params.get('credentials');
if (sessionsParam) {
try {
bolt.sessions = JSON.parse(sessionsParam) as Session[];
} catch (e) {
logger.error('Unable to parse saved credentials');
}
}
}
// TODO: refactor to not use listeners
function addWindowListeners(): void {
const env = bolt.env;
const origin = env.origin;
const clientId = env.clientid;
const origin_2fa = env.origin_2fa;
const boltUrl = get(internalUrl);
const exchangeUrl = origin.concat('/oauth2/token');
if (get(credentials).size == 0) {
if (bolt.sessions.length == 0) {
showDisclaimer.set(true);
}
@@ -53,10 +74,10 @@ export function start(): void {
AuthService.pendingOauth = null;
const post_data = new URLSearchParams({
grant_type: 'authorization_code',
client_id: bolt.clientid,
client_id: env.clientid,
code: event.data.code,
code_verifier: <string>pending.verifier,
redirect_uri: bolt.redirect
redirect_uri: env.redirect
});
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
@@ -66,10 +87,7 @@ export function start(): void {
if (creds) {
handleLogin(<Window>pending?.win, creds).then((x) => {
if (x) {
credentials.update((data) => {
data.set(creds.sub, creds);
return data;
});
bolt.sessions.push(creds);
BoltService.saveAllCreds();
}
});
@@ -120,22 +138,19 @@ export function start(): void {
logger.error('Incorrect nonce in id_token');
break;
}
const sessionsUrl = bolt.auth_api.concat('/sessions');
const sessionsUrl = env.auth_api.concat('/sessions');
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
if (xml.status == 200) {
const accountsUrl = bolt.auth_api.concat('/accounts');
const accountsUrl = env.auth_api.concat('/accounts');
pending!.creds!.session_id = JSON.parse(xml.response).sessionId;
handleNewSessionId(
<Credentials>pending!.creds,
<Session>pending!.creds,
accountsUrl,
<Promise<Account>>pending!.account_info_promise
).then((x) => {
if (x) {
credentials.update((data) => {
data.set(<string>pending?.creds?.sub, <Credentials>pending!.creds);
return data;
});
bolt.sessions.push(<Session>pending!.creds);
BoltService.saveAllCreds();
}
});
@@ -160,33 +175,26 @@ export function start(): void {
});
(async () => {
if (get(credentials).size > 0) {
get(credentials).forEach(async (value) => {
if (bolt.sessions.length > 0) {
bolt.sessions.forEach(async (value) => {
const result = await AuthService.checkRenewCreds(value, exchangeUrl, clientId);
if (result !== null && result !== 0) {
logger.error(`Discarding expired login for #${value.sub}`);
credentials.update((data) => {
data.delete(value.sub);
return data;
});
const index = bolt.sessions.findIndex((session) => session.sub === value.sub);
if (index > -1) bolt.sessions.splice(index, 1);
BoltService.saveAllCreds();
}
let checkedCred: Record<string, Credentials | boolean>;
let checkedCred: Record<string, Session | boolean>;
if (result === null && (await handleLogin(null, value))) {
checkedCred = { creds: value, valid: true };
} else {
checkedCred = { creds: value, valid: result === 0 };
}
if (checkedCred.valid) {
const creds = <Credentials>value;
credentials.update((data) => {
data.set(creds.sub, creds);
return data;
});
bolt.sessions.push(value);
BoltService.saveAllCreds();
}
});
}
isConfigDirty.set(false); // overrides all cases where this gets set to "true" due to loading existing config values
})();
}