aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLewis Crichton <lewi@lewisakura.moe>2023-04-07 01:27:18 +0100
committerGitHub <noreply@github.com>2023-04-07 02:27:18 +0200
commit97f8d4d5154d566568fc475d6aaba5db07399b2b (patch)
treefeafd22c26bdfc37a2d787e60f52bec94e777636 /src
parent2672dea8e361e8216b9459ac5dac97d36a47e412 (diff)
downloadVencord-97f8d4d5154d566568fc475d6aaba5db07399b2b.tar.gz
Vencord-97f8d4d5154d566568fc475d6aaba5db07399b2b.tar.bz2
Vencord-97f8d4d5154d566568fc475d6aaba5db07399b2b.zip
feat: Cloud settings sync (#505)
Co-authored-by: Ven <vendicated@riseup.net>
Diffstat (limited to 'src')
-rw-r--r--src/Vencord.ts29
-rw-r--r--src/api/settings.ts27
-rw-r--r--src/components/VencordSettings/CloudTab.tsx164
-rw-r--r--src/components/VencordSettings/index.tsx4
-rw-r--r--src/components/VencordSettings/settingsStyles.css11
-rw-r--r--src/plugins/settings.tsx7
-rw-r--r--src/utils/cloud.tsx124
-rw-r--r--src/utils/localStorage.ts19
-rw-r--r--src/utils/settingsSync.ts191
9 files changed, 564 insertions, 12 deletions
diff --git a/src/Vencord.ts b/src/Vencord.ts
index 73b53e8..a23b1a8 100644
--- a/src/Vencord.ts
+++ b/src/Vencord.ts
@@ -30,17 +30,44 @@ import "./webpack/patchWebpack";
import { showNotification } from "./api/Notifications";
import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
-import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater";
+import { localStorage } from "./utils/localStorage";
+import { getCloudSettings, putCloudSettings } from "./utils/settingsSync";
+import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
export let Components: any;
+async function syncSettings() {
+ if (
+ Settings.cloud.settingsSync && // if it's enabled
+ Settings.cloud.authenticated // if cloud integrations are enabled
+ ) {
+ if (localStorage.Vencord_settingsDirty) {
+ await putCloudSettings();
+ delete localStorage.Vencord_settingsDirty;
+ } else if (await getCloudSettings(false)) { // if we synchronized something (false means no sync)
+ // we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
+ // potential notifications that might occur. getCloudSettings() will always send a notification regardless if
+ // there was an error to notify the user, but besides that we only want to show one notification instead of all
+ // of the possible ones it has (such as when your settings are newer).
+ showNotification({
+ title: "Cloud Settings",
+ body: "Your settings have been updated! Click here to restart to fully apply changes!",
+ color: "var(--green-360)",
+ onClick: () => window.DiscordNative.app.relaunch()
+ });
+ }
+ }
+}
+
async function init() {
await onceReady;
startAllPlugins();
Components = await import("./components");
+ syncSettings();
+
if (!IS_WEB) {
try {
const isOutdated = await checkForUpdates();
diff --git a/src/api/settings.ts b/src/api/settings.ts
index 321a4c4..8a7d9ff 100644
--- a/src/api/settings.ts
+++ b/src/api/settings.ts
@@ -16,9 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
+import { localStorage } from "@utils/localStorage";
import Logger from "@utils/Logger";
import { mergeDefaults } from "@utils/misc";
+import { putCloudSettings } from "@utils/settingsSync";
import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types";
import { React } from "@webpack/common";
@@ -49,6 +52,13 @@ export interface Settings {
useNative: "always" | "never" | "not-focused";
logLimit: number;
};
+
+ cloud: {
+ authenticated: boolean;
+ url: string;
+ settingsSync: boolean;
+ settingsSyncVersion: number;
+ };
}
const DefaultSettings: Settings = {
@@ -69,6 +79,13 @@ const DefaultSettings: Settings = {
position: "bottom-right",
useNative: "not-focused",
logLimit: 50
+ },
+
+ cloud: {
+ authenticated: false,
+ url: "https://api.vencord.dev/",
+ settingsSync: false,
+ settingsSyncVersion: 0
}
};
@@ -80,6 +97,13 @@ try {
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
}
+const saveSettingsOnFrequentAction = debounce(async () => {
+ if (Settings.cloud.settingsSync && Settings.cloud.authenticated) {
+ await putCloudSettings();
+ delete localStorage.Vencord_settingsDirty;
+ }
+}, 60_000);
+
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
const subscriptions = new Set<SubscriptionCallback>();
@@ -142,6 +166,9 @@ function makeProxy(settings: any, root = settings, path = ""): Settings {
}
}
// And don't forget to persist the settings!
+ PlainSettings.cloud.settingsSyncVersion = Date.now();
+ localStorage.Vencord_settingsDirty = true;
+ saveSettingsOnFrequentAction();
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
return true;
}
diff --git a/src/components/VencordSettings/CloudTab.tsx b/src/components/VencordSettings/CloudTab.tsx
new file mode 100644
index 0000000..3452cef
--- /dev/null
+++ b/src/components/VencordSettings/CloudTab.tsx
@@ -0,0 +1,164 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { showNotification } from "@api/Notifications";
+import { Settings, useSettings } from "@api/settings";
+import { CheckedTextInput } from "@components/CheckedTextInput";
+import ErrorBoundary from "@components/ErrorBoundary";
+import { Link } from "@components/Link";
+import { authorizeCloud, cloudLogger, deauthorizeCloud, getCloudAuth, getCloudUrl } from "@utils/cloud";
+import { Margins } from "@utils/margins";
+import { deleteCloudSettings, getCloudSettings, putCloudSettings } from "@utils/settingsSync";
+import { Alerts, Button, Forms, Switch, Tooltip } from "@webpack/common";
+
+function validateUrl(url: string) {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return "Invalid URL";
+ }
+}
+
+async function eraseAllData() {
+ const res = await fetch(new URL("/v1/", getCloudUrl()), {
+ method: "DELETE",
+ headers: new Headers({
+ Authorization: await getCloudAuth()
+ })
+ });
+
+ if (!res.ok) {
+ cloudLogger.error(`Failed to erase data, API returned ${res.status}`);
+ showNotification({
+ title: "Cloud Integrations",
+ body: `Could not erase all data (API returned ${res.status}), please contact support.`,
+ color: "var(--red-360)"
+ });
+ return;
+ }
+
+ Settings.cloud.authenticated = false;
+ await deauthorizeCloud();
+
+ showNotification({
+ title: "Cloud Integrations",
+ body: "Successfully erased all data.",
+ color: "var(--green-360)"
+ });
+}
+
+function SettingsSyncSection() {
+ const { cloud } = useSettings(["cloud.authenticated", "cloud.settingsSync"]);
+ const sectionEnabled = cloud.authenticated && cloud.settingsSync;
+
+ return (
+ <Forms.FormSection title="Settings Sync" className={Margins.top16}>
+ <Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
+ Synchronize your settings to the cloud. This allows easy synchronization across multiple devices with
+ minimal effort.
+ </Forms.FormText>
+ <Switch
+ key="cloud-sync"
+ disabled={!cloud.authenticated}
+ value={cloud.settingsSync}
+ onChange={v => { cloud.settingsSync = v; }}
+ >
+ Settings Sync
+ </Switch>
+ <div className="vc-cloud-settings-sync-grid">
+ <Button
+ size={Button.Sizes.SMALL}
+ disabled={!sectionEnabled}
+ onClick={() => putCloudSettings()}
+ >Sync to Cloud</Button>
+ <Tooltip text="This will overwrite your local settings with the ones on the cloud. Use wisely!">
+ {({ onMouseLeave, onMouseEnter }) => (
+ <Button
+ onMouseLeave={onMouseLeave}
+ onMouseEnter={onMouseEnter}
+ size={Button.Sizes.SMALL}
+ color={Button.Colors.RED}
+ disabled={!sectionEnabled}
+ onClick={() => getCloudSettings(true, true)}
+ >Sync from Cloud</Button>
+ )}
+ </Tooltip>
+ <Button
+ size={Button.Sizes.SMALL}
+ color={Button.Colors.RED}
+ disabled={!sectionEnabled}
+ onClick={() => deleteCloudSettings()}
+ >Delete Cloud Settings</Button>
+ </div>
+ </Forms.FormSection>
+ );
+}
+
+function CloudTab() {
+ const settings = useSettings(["cloud.authenticated", "cloud.url"]);
+
+ return (
+ <>
+ <Forms.FormSection title="Cloud Settings" className={Margins.top16}>
+ <Forms.FormText variant="text-md/normal" className={Margins.bottom20}>
+ Vencord comes with a cloud integration that adds goodies like settings sync across devices.
+ It <Link href="https://vencord.dev/cloud/privacy">respects your privacy</Link>, and
+ the <Link href="https://github.com/Vencord/Backend">source code</Link> is AGPL 3.0 licensed so you
+ can host it yourself.
+ </Forms.FormText>
+ <Switch
+ key="backend"
+ value={settings.cloud.authenticated}
+ onChange={v => { v && authorizeCloud(); if (!v) settings.cloud.authenticated = v; }}
+ note="This will request authorization if you have not yet set up cloud integrations."
+ >
+ Enable Cloud Integrations
+ </Switch>
+ <Forms.FormTitle tag="h5">Backend URL</Forms.FormTitle>
+ <Forms.FormText className={Margins.bottom8}>
+ Which backend to use when using cloud integrations.
+ </Forms.FormText>
+ <CheckedTextInput
+ key="backendUrl"
+ value={settings.cloud.url}
+ onChange={v => { settings.cloud.url = v; settings.cloud.authenticated = false; deauthorizeCloud(); }}
+ validate={validateUrl}
+ />
+ <Button
+ className={Margins.top8}
+ size={Button.Sizes.MEDIUM}
+ color={Button.Colors.RED}
+ disabled={!settings.cloud.authenticated}
+ onClick={() => Alerts.show({
+ title: "Are you sure?",
+ body: "Once your data is erased, we cannot recover it. There's no going back!",
+ onConfirm: eraseAllData,
+ confirmText: "Erase it!",
+ confirmColor: "vc-cloud-erase-data-danger-btn",
+ cancelText: "Nevermind"
+ })}
+ >Erase All Data</Button>
+ <Forms.FormDivider className={Margins.top16} />
+ </Forms.FormSection >
+ <SettingsSyncSection />
+ </>
+ );
+}
+
+export default ErrorBoundary.wrap(CloudTab);
diff --git a/src/components/VencordSettings/index.tsx b/src/components/VencordSettings/index.tsx
index cd6ce60..c15944c 100644
--- a/src/components/VencordSettings/index.tsx
+++ b/src/components/VencordSettings/index.tsx
@@ -24,6 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed";
import { Forms, SettingsRouter, TabBar, Text } from "@webpack/common";
import BackupRestoreTab from "./BackupRestoreTab";
+import CloudTab from "./CloudTab";
import PluginsTab from "./PluginsTab";
import ThemesTab from "./ThemesTab";
import Updater from "./Updater";
@@ -45,7 +46,8 @@ const SettingsTabs: Record<string, SettingsTab> = {
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
- VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
+ VencordCloud: { name: "Cloud", component: () => <CloudTab /> },
+ VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> }
};
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css
index 709c823..ebc112c 100644
--- a/src/components/VencordSettings/settingsStyles.css
+++ b/src/components/VencordSettings/settingsStyles.css
@@ -46,3 +46,14 @@
padding: 0.5em;
border: 1px solid var(--background-modifier-accent);
}
+
+.vc-cloud-settings-sync-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ grid-gap: 1em;
+}
+
+.vc-cloud-erase-data-danger-btn {
+ color: var(--white-500);
+ background-color: var(--button-danger-background);
+}
diff --git a/src/plugins/settings.tsx b/src/plugins/settings.tsx
index 8db0a4e..c8a6372 100644
--- a/src/plugins/settings.tsx
+++ b/src/plugins/settings.tsx
@@ -107,6 +107,13 @@ export default definePlugin({
});
cats.push({
+ section: "VencordCloud",
+ label: "Cloud",
+ element: () => <SettingsComponent tab="VencordCloud" />,
+ onClick: makeOnClick("VencordCloud")
+ });
+
+ cats.push({
section: "VencordSettingsSync",
label: "Backup & Restore",
element: () => <SettingsComponent tab="VencordSettingsSync" />,
diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx
new file mode 100644
index 0000000..b31091f
--- /dev/null
+++ b/src/utils/cloud.tsx
@@ -0,0 +1,124 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import * as DataStore from "@api/DataStore";
+import { showNotification } from "@api/Notifications";
+import { Settings } from "@api/settings";
+import { findByProps } from "@webpack";
+import { UserStore } from "@webpack/common";
+
+import Logger from "./Logger";
+import { openModal } from "./modal";
+
+export const cloudLogger = new Logger("Cloud", "#39b7e0");
+export const getCloudUrl = () => new URL(Settings.cloud.url);
+
+export async function getAuthorization() {
+ const secrets = await DataStore.get<Record<string, string>>("Vencord_cloudSecret") ?? {};
+ return secrets[getCloudUrl().origin];
+}
+
+async function setAuthorization(secret: string) {
+ await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
+ secrets ??= {};
+ secrets[getCloudUrl().origin] = secret;
+ return secrets;
+ });
+}
+
+export async function deauthorizeCloud() {
+ await DataStore.update<Record<string, string>>("Vencord_cloudSecret", secrets => {
+ secrets ??= {};
+ delete secrets[getCloudUrl().origin];
+ return secrets;
+ });
+}
+
+export async function authorizeCloud() {
+ if (await getAuthorization() !== undefined) {
+ Settings.cloud.authenticated = true;
+ return;
+ }
+
+ try {
+ const oauthConfiguration = await fetch(new URL("/v1/oauth/settings", getCloudUrl()));
+ var { clientId, redirectUri } = await oauthConfiguration.json();
+ } catch {
+ showNotification({
+ title: "Cloud Integration",
+ body: "Setup failed (couldn't retrieve OAuth configuration)."
+ });
+ Settings.cloud.authenticated = false;
+ return;
+ }
+
+ const { OAuth2AuthorizeModal } = findByProps("OAuth2AuthorizeModal");
+
+ openModal((props: any) => <OAuth2AuthorizeModal
+ {...props}
+ scopes={["identify"]}
+ responseType="code"
+ redirectUri={redirectUri}
+ permissions={0n}
+ clientId={clientId}
+ cancelCompletesFlow={false}
+ callback={async (callbackUrl: string) => {
+ if (!callbackUrl) {
+ Settings.cloud.authenticated = false;
+ return;
+ }
+
+ try {
+ const res = await fetch(callbackUrl, {
+ headers: new Headers({ Accept: "application/json" })
+ });
+ const { secret } = await res.json();
+ if (secret) {
+ cloudLogger.info("Authorized with secret");
+ await setAuthorization(secret);
+ showNotification({
+ title: "Cloud Integration",
+ body: "Cloud integrations enabled!"
+ });
+ Settings.cloud.authenticated = true;
+ } else {
+ showNotification({
+ title: "Cloud Integration",
+ body: "Setup failed (no secret returned?)."
+ });
+ Settings.cloud.authenticated = false;
+ }
+ } catch (e: any) {
+ cloudLogger.error("Failed to authorize", e);
+ showNotification({
+ title: "Cloud Integration",
+ body: `Setup failed (${e.toString()}).`
+ });
+ Settings.cloud.authenticated = false;
+ }
+ }
+ }
+ />);
+}
+
+export async function getCloudAuth() {
+ const userId = UserStore.getCurrentUser().id;
+ const secret = await getAuthorization();
+
+ return window.btoa(`${secret}:${userId}`);
+}
diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts
new file mode 100644
index 0000000..8730bb2
--- /dev/null
+++ b/src/utils/localStorage.ts
@@ -0,0 +1,19 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export const { localStorage } = window;
diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts
index 781899f..d59787d 100644
--- a/src/utils/settingsSync.ts
+++ b/src/utils/settingsSync.ts
@@ -16,8 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+import { showNotification } from "@api/Notifications";
+import { PlainSettings, Settings } from "@api/settings";
import { Toasts } from "@webpack/common";
+import { deflateSync, inflateSync } from "fflate";
+import { getCloudAuth, getCloudUrl } from "./cloud";
import IpcEvents from "./IpcEvents";
import Logger from "./Logger";
@@ -64,17 +68,18 @@ export async function downloadSettingsBackup() {
}
}
-const toastSuccess = () => Toasts.show({
- type: Toasts.Type.SUCCESS,
- message: "Settings successfully imported. Restart to apply changes!",
- id: Toasts.genId()
-});
+const toast = (type: number, message: string) =>
+ Toasts.show({
+ type,
+ message,
+ id: Toasts.genId()
+ });
-const toastFailure = (err: any) => Toasts.show({
- type: Toasts.Type.FAILURE,
- message: `Failed to import settings: ${String(err)}`,
- id: Toasts.genId()
-});
+const toastSuccess = () =>
+ toast(Toasts.Type.SUCCESS, "Settings successfully imported. Restart to apply changes!");
+
+const toastFailure = (err: any) =>
+ toast(Toasts.Type.FAILURE, `Failed to import settings: ${String(err)}`);
export async function uploadSettingsBackup(showToast = true): Promise<void> {
if (IS_DISCORD_DESKTOP) {
@@ -121,3 +126,169 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> {
setImmediate(() => document.body.removeChild(input));
}
}
+
+// Cloud settings
+const cloudSettingsLogger = new Logger("Cloud:Settings", "#39b7e0");
+
+export async function putCloudSettings() {
+ const settings = await exportSettings();
+
+ try {
+ const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
+ method: "PUT",
+ headers: new Headers({
+ Authorization: await getCloudAuth(),
+ "Content-Type": "application/octet-stream"
+ }),
+ body: deflateSync(new TextEncoder().encode(settings))
+ });
+
+ if (!res.ok) {
+ cloudSettingsLogger.error(`Failed to sync up, API returned ${res.status}`);
+ showNotification({
+ title: "Cloud Settings",
+ body: `Could not synchronize settings to cloud (API returned ${res.status}).`,
+ color: "var(--red-360)"
+ });
+ return;
+ }
+
+ const { written } = await res.json();
+ PlainSettings.cloud.settingsSyncVersion = written;
+ VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(PlainSettings, null, 4));
+
+ cloudSettingsLogger.info("Settings uploaded to cloud successfully");
+ showNotification({
+ title: "Cloud Settings",
+ body: "Synchronized your settings to the cloud!",
+ color: "var(--green-360)"
+ });
+ } catch (e: any) {
+ cloudSettingsLogger.error("Failed to sync up", e);
+ showNotification({
+ title: "Cloud Settings",
+ body: `Could not synchronize settings to the cloud (${e.toString()}).`,
+ color: "var(--red-360)"
+ });
+ }
+}
+
+export async function getCloudSettings(shouldNotify = true, force = false) {
+ try {
+ const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
+ method: "GET",
+ headers: new Headers({
+ Authorization: await getCloudAuth(),
+ Accept: "application/octet-stream",
+ "If-None-Match": Settings.cloud.settingsSyncVersion.toString()
+ }),
+ });
+
+ if (res.status === 404) {
+ cloudSettingsLogger.info("No settings on the cloud");
+ if (shouldNotify)
+ showNotification({
+ title: "Cloud Settings",
+ body: "There are no settings in the cloud."
+ });
+ return false;
+ }
+
+ if (res.status === 304) {
+ cloudSettingsLogger.info("Settings up to date");
+ if (shouldNotify)
+ showNotification({
+ title: "Cloud Settings",
+ body: "Your settings are up to date."
+ });
+ return false;
+ }
+
+ if (!res.ok) {
+ cloudSettingsLogger.error(`Failed to sync down, API returned ${res.status}`);
+ showNotification({
+ title: "Cloud Settings",
+ body: `Could not synchronize settings from the cloud (API returned ${res.status}).`,
+ color: "var(--red-360)"
+ });
+ return false;
+ }
+
+ const written = Number(res.headers.get("etag")!);
+ const localWritten = Settings.cloud.settingsSyncVersion;
+
+ // don't need to check for written > localWritten because the server will return 304 due to if-none-match
+ if (!force && written < localWritten) {
+ if (shouldNotify)
+ showNotification({
+ title: "Cloud Settings",
+ body: "Your local settings are newer than the cloud ones."
+ });
+ return;
+ }
+
+ const data = await res.arrayBuffer();
+
+ const settings = new TextDecoder().decode(inflateSync(new Uint8Array(data)));
+ await importSettings(settings);
+
+ // sync with server timestamp instead of local one
+ PlainSettings.cloud.settingsSyncVersion = written;
+ VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(PlainSettings, null, 4));
+
+ cloudSettingsLogger.info("Settings loaded from cloud successfully");
+ if (shouldNotify)
+ showNotification({
+ title: "Cloud Settings",
+ body: "Your settings have been updated! Click here to restart to fully apply changes!",
+ color: "var(--green-360)",
+ onClick: () => window.DiscordNative.app.relaunch()
+ });
+
+ return true;
+ } catch (e: any) {
+ cloudSettingsLogger.error("Failed to sync down", e);
+ showNotification({
+ title: "Cloud Settings",
+ body: `Could not synchronize settings from the cloud (${e.toString()}).`,
+ color: "var(--red-360)"
+ });
+
+ return false;
+ }
+}
+
+export async function deleteCloudSettings() {
+ try {
+ const res = await fetch(new URL("/v1/settings", getCloudUrl()), {
+ method: "DELETE",
+ headers: new Headers({
+ Authorization: await getCloudAuth()
+ }),
+ });
+
+ if (!res.ok) {
+ cloudSettingsLogger.error(`Failed to delete, API returned ${res.status}`);
+ showNotification({
+ title: "Cloud Settings",
+ body: `Could not delete settings (API returned ${res.status}).`,
+ color: "var(--red-360)"
+ });
+ return;
+ }
+
+ cloudSettingsLogger.info("Settings deleted from cloud successfully");
+ showNotification({
+ title: "Cloud Settings",
+ body: "Settings deleted from cloud!",
+ color: "var(--green-360)"
+ });
+ } catch (e: any) {
+ cloudSettingsLogger.error("Failed to delete", e);
+ showNotification({
+ title: "Cloud Settings",
+ body: `Could not delete settings (${e.toString()}).`,
+ color: "var(--red-360)"
+ });
+ }
+}