aboutsummaryrefslogtreecommitdiff
path: root/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/cloud.tsx124
-rw-r--r--src/utils/localStorage.ts19
-rw-r--r--src/utils/settingsSync.ts191
3 files changed, 324 insertions, 10 deletions
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)"
+ });
+ }
+}