2022-11-21 19:25:40 +01:00
/ *
* Vencord , a modification for Discord ' s desktop app
* Copyright ( c ) 2022 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/>.
* /
2025-02-11 12:58:05 +01:00
import { DataStore } from "@api/index" ;
2023-04-07 01:27:18 +01:00
import { showNotification } from "@api/Notifications" ;
2023-05-06 01:36:00 +02:00
import { PlainSettings , Settings } from "@api/Settings" ;
2025-03-31 17:36:21 -04:00
import { moment , SettingsRouter , Toasts } from "@webpack/common" ;
2023-04-07 01:27:18 +01:00
import { deflateSync , inflateSync } from "fflate" ;
2022-11-28 13:37:55 +01:00
2025-09-10 01:45:41 -04:00
import { getCloudAuth , getCloudUrl } from "./cloud" ;
2023-05-06 01:36:00 +02:00
import { Logger } from "./Logger" ;
2023-09-24 01:16:41 +02:00
import { relaunch } from "./native" ;
2023-07-26 01:27:04 +02:00
import { chooseFile , saveFile } from "./web" ;
2022-11-21 19:25:40 +01:00
export async function importSettings ( data : string ) {
try {
var parsed = JSON . parse ( data ) ;
} catch ( err ) {
console . log ( data ) ;
throw new Error ( "Failed to parse JSON: " + String ( err ) ) ;
}
2025-02-12 15:24:33 -07:00
if ( "settings" in parsed && "quickCss" in parsed ) {
2023-04-15 13:50:00 +01:00
Object . assign ( PlainSettings , parsed . settings ) ;
2024-03-13 21:45:45 +01:00
await VencordNative . settings . set ( parsed . settings ) ;
2023-05-02 02:50:51 +02:00
await VencordNative . quickCss . set ( parsed . quickCss ) ;
2025-02-12 15:24:33 -07:00
if ( parsed . dataStore ) await DataStore . setMany ( parsed . dataStore ) ;
2022-11-21 19:25:40 +01:00
} else
2025-02-11 12:58:05 +01:00
throw new Error ( "Invalid Settings. Is this even an Equicord Settings file?" ) ;
2022-11-21 19:25:40 +01:00
}
2023-07-04 23:57:28 +08:00
export async function exportSettings ( { minify } : { minify? : boolean ; } = { } ) {
2024-03-13 21:45:45 +01:00
const settings = VencordNative . settings . get ( ) ;
2023-05-02 02:50:51 +02:00
const quickCss = await VencordNative . quickCss . get ( ) ;
2025-02-11 12:58:05 +01:00
const dataStore = await DataStore . entries ( ) ;
return JSON . stringify ( { settings , quickCss , dataStore } , null , minify ? undefined : 4 ) ;
2022-11-21 19:25:40 +01:00
}
export async function downloadSettingsBackup() {
2024-07-16 00:15:41 -04:00
const filename = ` equicord-settings-backup- ${ moment ( ) . format ( "YYYY-MM-DD" ) } .json ` ;
2022-11-21 19:25:40 +01:00
const backup = await exportSettings ( ) ;
const data = new TextEncoder ( ) . encode ( backup ) ;
2023-04-04 01:16:29 +02:00
if ( IS_DISCORD_DESKTOP ) {
DiscordNative . fileManager . saveWithDialog ( data , filename ) ;
} else {
2023-04-08 03:51:37 +02:00
saveFile ( new File ( [ data ] , filename , { type : "application/json" } ) ) ;
2022-11-21 19:25:40 +01:00
}
}
2025-02-06 20:04:59 +00:00
const toast = ( type : string , message : string ) = >
2023-04-07 01:27:18 +01:00
Toasts . show ( {
type ,
message ,
id : Toasts.genId ( )
} ) ;
2022-11-21 19:25:40 +01:00
2023-04-07 01:27:18 +01:00
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 ) } ` ) ;
2022-11-21 19:25:40 +01:00
export async function uploadSettingsBackup ( showToast = true ) : Promise < void > {
2023-04-04 01:16:29 +02:00
if ( IS_DISCORD_DESKTOP ) {
const [ file ] = await DiscordNative . fileManager . openFiles ( {
filters : [
2024-04-17 14:29:47 -04:00
{ name : "Equicord Settings Backup" , extensions : [ "json" ] } ,
2023-04-04 01:16:29 +02:00
{ name : "all" , extensions : [ "*" ] }
]
} ) ;
if ( file ) {
try {
await importSettings ( new TextDecoder ( ) . decode ( file . data ) ) ;
if ( showToast ) toastSuccess ( ) ;
} catch ( err ) {
new Logger ( "SettingsSync" ) . error ( err ) ;
if ( showToast ) toastFailure ( err ) ;
}
}
} else {
2023-07-26 01:27:04 +02:00
const file = await chooseFile ( "application/json" ) ;
if ( ! file ) return ;
2022-11-21 19:25:40 +01:00
2023-07-26 01:27:04 +02:00
const reader = new FileReader ( ) ;
reader . onload = async ( ) = > {
try {
await importSettings ( reader . result as string ) ;
if ( showToast ) toastSuccess ( ) ;
} catch ( err ) {
new Logger ( "SettingsSync" ) . error ( err ) ;
if ( showToast ) toastFailure ( err ) ;
}
} ;
reader . readAsText ( file ) ;
2022-11-21 19:25:40 +01:00
}
}
2023-04-07 01:27:18 +01:00
// Cloud settings
const cloudSettingsLogger = new Logger ( "Cloud:Settings" , "#39b7e0" ) ;
2023-07-04 23:59:42 +08:00
export async function putCloudSettings ( manual? : boolean ) {
2023-07-04 23:57:28 +08:00
const settings = await exportSettings ( { minify : true } ) ;
2023-04-07 01:27:18 +01:00
try {
const res = await fetch ( new URL ( "/v1/settings" , getCloudUrl ( ) ) , {
method : "PUT" ,
2024-02-29 16:26:32 -08:00
headers : {
2023-04-07 01:27:18 +01:00
Authorization : await getCloudAuth ( ) ,
"Content-Type" : "application/octet-stream"
2024-02-29 16:26:32 -08:00
} ,
2023-04-07 01:27:18 +01:00
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 ;
2024-03-13 21:45:45 +01:00
VencordNative . settings . set ( PlainSettings ) ;
2023-04-07 01:27:18 +01:00
cloudSettingsLogger . info ( "Settings uploaded to cloud successfully" ) ;
2023-07-04 23:59:42 +08:00
if ( manual ) {
showNotification ( {
title : "Cloud Settings" ,
body : "Synchronized settings to the cloud!" ,
noPersist : true ,
} ) ;
}
2023-04-07 01:27:18 +01:00
} 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" ,
2024-02-29 16:26:32 -08:00
headers : {
2023-04-07 01:27:18 +01:00
Authorization : await getCloudAuth ( ) ,
Accept : "application/octet-stream" ,
"If-None-Match" : Settings . cloud . settingsSyncVersion . toString ( )
2024-02-29 16:26:32 -08:00
} ,
2023-04-07 01:27:18 +01:00
} ) ;
2025-03-31 17:36:21 -04:00
if ( res . status === 401 ) {
// User switched to an account that isn't connected to cloud
showNotification ( {
title : "Cloud Settings" ,
body : "Cloud sync was disabled because this account isn't connected to the Vencloud App. You can enable it again by connecting this account in Cloud Settings. (note: it will store your preferences separately)" ,
color : "var(--yellow-360)" ,
onClick : ( ) = > SettingsRouter . open ( "VencordCloud" )
} ) ;
// Disable cloud sync globally
Settings . cloud . authenticated = false ;
return false ;
}
2023-04-07 01:27:18 +01:00
if ( res . status === 404 ) {
cloudSettingsLogger . info ( "No settings on the cloud" ) ;
if ( shouldNotify )
showNotification ( {
title : "Cloud Settings" ,
2023-04-13 19:15:36 +02:00
body : "There are no settings in the cloud." ,
noPersist : true
2023-04-07 01:27:18 +01:00
} ) ;
return false ;
}
if ( res . status === 304 ) {
cloudSettingsLogger . info ( "Settings up to date" ) ;
if ( shouldNotify )
showNotification ( {
title : "Cloud Settings" ,
2023-04-13 19:15:36 +02:00
body : "Your settings are up to date." ,
noPersist : true
2023-04-07 01:27:18 +01:00
} ) ;
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" ,
2023-04-13 19:15:36 +02:00
body : "Your local settings are newer than the cloud ones." ,
noPersist : true ,
2023-04-07 01:27:18 +01:00
} ) ;
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 ;
2024-03-13 21:45:45 +01:00
VencordNative . settings . set ( PlainSettings ) ;
2023-04-07 01:27:18 +01:00
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)" ,
2023-09-24 01:16:41 +02:00
onClick : IS_WEB ? ( ) = > location . reload ( ) : relaunch ,
2023-04-13 19:15:36 +02:00
noPersist : true
2023-04-07 01:27:18 +01:00
} ) ;
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" ,
2024-02-29 16:26:32 -08:00
headers : { Authorization : await getCloudAuth ( ) } ,
2023-04-07 01:27:18 +01:00
} ) ;
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)"
} ) ;
}
}