diff options
Diffstat (limited to 'src/utils')
-rw-r--r-- | src/utils/cloud.tsx | 124 | ||||
-rw-r--r-- | src/utils/localStorage.ts | 19 | ||||
-rw-r--r-- | src/utils/settingsSync.ts | 191 |
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)" + }); + } +} |