@@ -26,6 +26,67 @@ import ProgressBar from 'electron-progressbar';
2626import { exec , spawn } from 'child_process' ;
2727import decompress from 'decompress' ;
2828import { promisify } from 'util' ;
29+ import { net , safeStorage } from 'electron' ;
30+
31+ function semVer ( version : string ) {
32+ const pattern = / ( \d + ) \. ( \d + ) \. ( \d + ) / ;
33+ let [ , major , minor , patch ] = pattern . exec ( version ) ;
34+ return { major : + major , minor : + minor , patch : + patch } ;
35+ }
36+ function semVerLess ( x : string , y : string ) : boolean {
37+ let xver = semVer ( x ) , yver = semVer ( y ) ;
38+ return xver . major < yver . major ||
39+ xver . major === yver . major && (
40+ xver . minor < yver . minor ||
41+ xver . minor === yver . minor && xver . patch < yver . patch
42+ ) ;
43+ }
44+
45+ const backendAPI = 'https://minskybe-x7dj1.sevalla.app/api' ;
46+ // perform a call on the backend API, returning the JSON encoded result
47+ // options is passed to the constructor of a ClienRequest object https://www.electronjs.org/docs/latest/api/client-request#requestendchunk-encoding-callback
48+ async function callBackendAPI ( options : string | Object , token : string ) {
49+ return new Promise < string > ( ( resolve , reject ) => {
50+ let request = net . request ( options ) ;
51+ request . setHeader ( 'Authorization' , `Bearer ${ token } ` ) ;
52+ request . on ( 'response' , ( response ) => {
53+ let chunks = [ ] ;
54+ response . on ( 'data' , ( chunk ) => { chunks . push ( chunk ) ; } ) ;
55+ response . on ( 'end' , ( ) => resolve ( Buffer . concat ( chunks ) . toString ( ) ) ) ;
56+ response . on ( 'error' , ( ) => reject ( response . statusMessage ) ) ;
57+ } ) ;
58+ request . on ( 'error' , ( err ) => reject ( err . toString ( ) ) ) ;
59+ request . end ( ) ;
60+ } ) ;
61+ }
62+
63+ // to handle redirects
64+ async function getFinalUrl ( initialUrl , token ) {
65+ try {
66+ const response = await fetch ( initialUrl , {
67+ method : 'GET' ,
68+ headers : {
69+ 'Authorization' : `Bearer ${ token } `
70+ } ,
71+ redirect : 'manual' // This tells fetch NOT to follow the link automatically
72+ } ) ;
73+
74+ // In 'manual' mode, a redirect returns an 'opaqueredirect' type or status 302
75+ if ( response . status >= 300 && response . status < 400 ) {
76+ const redirectUrl = response . headers . get ( 'location' ) ;
77+ if ( redirectUrl ) return redirectUrl ;
78+ }
79+
80+ if ( response . ok ) return initialUrl ;
81+
82+ throw new Error ( `Server responded with ${ response . status } ` ) ;
83+ } catch ( error ) {
84+ // If redirect: 'manual' is used, fetch might throw a 'TypeError'
85+ // when it hits the redirect—this is actually what we want to catch.
86+ console . error ( "Fetch encountered the redirect/error:" , error ) ;
87+ throw error ;
88+ }
89+ }
2990
3091export class CommandsManager {
3192 static activeGodleyWindowItems = new Map < string , CanvasItem > ( ) ;
@@ -1137,6 +1198,7 @@ export class CommandsManager {
11371198
11381199 // handler for downloading Ravel and installing it
11391200 static downloadRavel ( event , item , webContents ) {
1201+
11401202 switch ( process . platform ) {
11411203 case 'win32' :
11421204 const savePath = dirname ( process . execPath ) + '/libravel.dll' ;
@@ -1158,7 +1220,7 @@ export class CommandsManager {
11581220 // handler for when download completed
11591221 item . once ( 'done' , ( event , state ) => {
11601222 progress . close ( ) ;
1161-
1223+
11621224 if ( state === 'completed' ) {
11631225 dialog . showMessageBoxSync ( WindowManager . getMainWindow ( ) , {
11641226 message : 'Ravel plugin updated successfully - restart Ravel to use' ,
@@ -1308,6 +1370,44 @@ export class CommandsManager {
13081370 modal : false ,
13091371 } ) ;
13101372 }
1373+
1374+ // return information about the current system
1375+ static async buildState ( previous : boolean ) {
1376+ // need to pass what platform we are
1377+ let state ;
1378+ switch ( process . platform ) {
1379+ case 'win32' :
1380+ state = { system : 'windows' , distro : '' , version : '' , arch :'' , previous : '' } ;
1381+ break ;
1382+ case 'darwin' :
1383+ state = { system : 'macos' , distro : '' , version : '' , arch : `${ process . arch } ` , previous : '' } ;
1384+ break ;
1385+ case 'linux' : {
1386+ state = { system : 'linux' , distro : '' , version : '' , arch :'' , previous : '' } ;
1387+ // figure out distro and version from /etc/os-release
1388+ let aexec = promisify ( exec ) ;
1389+ let osRelease = '/etc/os-release' ;
1390+ if ( existsSync ( process . resourcesPath + '/os-release' ) )
1391+ osRelease = process . resourcesPath + '/os-release' ;
1392+ let distroInfo = await aexec ( `grep ^ID= ${ osRelease } ` ) ;
1393+ // value may or may not be quoted
1394+ let extractor = / .* = [ ' " ] ? ( [ ^ ' " \n ] * ) [ ' " ] ? / ;
1395+ state . distro = extractor . exec ( distroInfo . stdout ) [ 1 ] ;
1396+ distroInfo = await aexec ( `grep ^VERSION_ID= ${ osRelease } ` ) ;
1397+ state . version = extractor . exec ( distroInfo . stdout ) [ 1 ] ;
1398+ break ;
1399+ }
1400+ default :
1401+ dialog . showMessageBoxSync ( WindowManager . getMainWindow ( ) , {
1402+ message : `In app update is not available for your operating system yet, please check back later` ,
1403+ type : 'error' ,
1404+ } ) ;
1405+ return null ;
1406+ }
1407+ if ( await minsky . ravelAvailable ( ) && previous )
1408+ state . previous = / [ ^ : ] * / . exec ( await minsky . ravelVersion ( ) ) [ 0 ] ;
1409+ return state ;
1410+ }
13111411
13121412 static async upgrade ( installCase : InstallCase = InstallCase . theLot ) {
13131413 const window = this . createDownloadWindow ( ) ;
@@ -1344,7 +1444,7 @@ export class CommandsManager {
13441444 }
13451445 if ( ravelFile ) {
13461446 // currently on latest, so reinstall ravel
1347- window . webContents . session . on ( 'will-download' , this . downloadRavel ) ;
1447+ window . webContents . session . on ( 'will-download' , this . downloadRavel ) ;
13481448 window . webContents . downloadURL ( ravelFile ) ;
13491449 return ;
13501450 }
@@ -1357,44 +1457,87 @@ export class CommandsManager {
13571457 }
13581458 } ) ;
13591459
1360- let clientId = '-PiL7snNmZL_BlLJTPm62SHBcFTMG5d46m2336r118mfrp6sz4ty0g-thbKAs76c' ;
1361- // need to pass what platform we are
1362- let state ;
1363- switch ( process . platform ) {
1364- case 'win32' :
1365- state = { system : 'windows' , distro : '' , version : '' , arch :'' , previous : '' } ;
1366- break ;
1367- case 'darwin' :
1368- state = { system : 'macos' , distro : '' , version : '' , arch : `${ process . arch } ` , previous : '' } ;
1369- break ;
1370- case 'linux' :
1371- state = { system : 'linux' , distro : '' , version : '' , arch :'' , previous : '' } ;
1372- // figure out distro and version from /etc/os-release
1373- let aexec = promisify ( exec ) ;
1374- let osRelease = '/etc/os-release' ;
1375- if ( existsSync ( process . resourcesPath + '/os-release' ) )
1376- osRelease = process . resourcesPath + '/os-release' ;
1377- let distroInfo = await aexec ( `grep ^ID= ${ osRelease } ` ) ;
1378- // value may or may not be quoted
1379- let extractor = / .* = [ ' " ] ? ( [ ^ ' " \n ] * ) [ ' " ] ? / ;
1380- state . distro = extractor . exec ( distroInfo . stdout ) [ 1 ] ;
1381- distroInfo = await aexec ( `grep ^VERSION_ID= ${ osRelease } ` ) ;
1382- state . version = extractor . exec ( distroInfo . stdout ) [ 1 ] ;
1383- break ;
1384- default :
1385- dialog . showMessageBoxSync ( WindowManager . getMainWindow ( ) , {
1386- message : `In app update is not available for your operating system yet, please check back later` ,
1387- type : 'error' ,
1388- } ) ;
1460+ let state = await CommandsManager . buildState ( installCase == InstallCase . previousRavel ) ;
1461+ if ( ! state ) {
13891462 window . close ( ) ;
13901463 return ;
1391- break ;
13921464 }
1393- if ( await minsky . ravelAvailable ( ) && installCase === InstallCase . previousRavel )
1394- state . previous = / [ ^ : ] * / . exec ( await minsky . ravelVersion ( ) ) [ 0 ] ;
1465+ let clientId = '-PiL7snNmZL_BlLJTPm62SHBcFTMG5d46m2336r118mfrp6sz4ty0g-thbKAs76c' ;
13951466 let encodedState = encodeURI ( JSON . stringify ( state ) ) ;
13961467 // load patreon's login page
13971468 window . loadURL ( `https://www.patreon.com/oauth2/authorize?response_type=code&client_id=${ clientId } &redirect_uri=https://ravelation.net/ravel-downloader.cgi&scope=identity%20identity%5Bemail%5D&state=${ encodedState } ` ) ;
13981469 }
1470+
1471+
1472+ // gets release URL for current system from Ravelation.net backend
1473+ static async getRelease ( product : string , previous : boolean , token : string ) {
1474+ let state = await CommandsManager . buildState ( previous ) ;
1475+ if ( ! state ) return '' ;
1476+ let query = `product=${ product } &os=${ state . system } &arch=${ state . arch } &distro=${ state . distro } &distro_version=${ state . version } ` ;
1477+ if ( previous ) {
1478+ let releases = JSON . parse ( await callBackendAPI ( `${ backendAPI } /releases?${ query } ` , token ) ) ;
1479+ let prevRelease ;
1480+ for ( let release of releases )
1481+ if ( semVerLess ( release . version , state . previous ) )
1482+ prevRelease = release ;
1483+ if ( prevRelease ) return prevRelease . download_url ;
1484+ // if not, then treat the request as latest
1485+ }
1486+ let release = JSON . parse ( await callBackendAPI ( `${ backendAPI } /releases/latest?${ query } ` , token ) ) ;
1487+ return release ?. release ?. download_url ;
1488+ }
1489+
1490+ static stashClerkToken ( token : string ) {
1491+ if ( token ) {
1492+ if ( safeStorage . isEncryptionAvailable ( ) ) {
1493+ const encrypted = safeStorage . encryptString ( token ) ;
1494+ StoreManager . store . set ( 'authToken' , encrypted . toString ( 'latin1' ) ) ;
1495+ } else
1496+ // fallback: store plaintext
1497+ StoreManager . store . set ( 'authToken' , token ) ;
1498+ } else {
1499+ StoreManager . store . delete ( 'authToken' ) ;
1500+ }
1501+ }
13991502
1503+ static async upgradeUsingClerk ( installCase : InstallCase = InstallCase . theLot ) {
1504+ while ( ! StoreManager . store . get ( 'authToken' ) )
1505+ if ( ! await WindowManager . openLoginWindow ( ) ) return ;
1506+
1507+ let token = StoreManager . store . get ( 'authToken' ) ;
1508+ // decrypt token if encrypted
1509+ if ( safeStorage . isEncryptionAvailable ( ) )
1510+ token = safeStorage . decryptString ( Buffer . from ( token , 'latin1' ) ) ;
1511+
1512+ const window = WindowManager . getMainWindow ( ) ;
1513+ let minskyAsset ;
1514+ try {
1515+ if ( installCase === InstallCase . theLot )
1516+ minskyAsset = await CommandsManager . getRelease ( 'minsky' , false , token ) ;
1517+ let ravelAsset = await CommandsManager . getRelease ( 'ravel' , installCase === InstallCase . previousRavel , token ) ;
1518+
1519+ if ( minskyAsset ) {
1520+ if ( ravelAsset ) { // stash ravel upgrade to be installed on next startup
1521+ StoreManager . store . set ( 'ravelPlugin' , await getFinalUrl ( ravelAsset , token ) ) ;
1522+ }
1523+ window . webContents . session . on ( 'will-download' , this . downloadMinsky ) ;
1524+ window . webContents . downloadURL ( await getFinalUrl ( minskyAsset , token ) ) ;
1525+ return ;
1526+ } else if ( ravelAsset ) {
1527+ window . webContents . session . on ( 'will-download' , this . downloadRavel ) ;
1528+ window . webContents . downloadURL ( await getFinalUrl ( ravelAsset , token ) ) ;
1529+ return ;
1530+ }
1531+ dialog . showMessageBoxSync ( WindowManager . getMainWindow ( ) , {
1532+ message : "Everything's up to date, nothing to do.\n" +
1533+ "If you're trying to download the Ravel plugin, please ensure you are logged into an account subscribed to Ravel Fan or Explorer tiers." ,
1534+ type : 'info' ,
1535+ } ) ;
1536+ }
1537+ catch ( error ) {
1538+ dialog . showErrorBox ( 'Error' , error . toString ( ) ) ;
1539+ }
1540+
1541+ }
1542+
14001543}
0 commit comments