mirror of
https://github.com/dscalzi/HeliosLauncher.git
synced 2024-12-22 03:32:12 -08:00
Add mod verification logic using Athena's Shield
Integrated Athena's Shield for mod verification, including mod identity extraction and validation against expected identities. Added exclusion list for mods, and implemented fallbacks for missing mod identities using MD5 hashes. Adjusted logging and error reporting to provide clearer feedback.
This commit is contained in:
parent
1476dbb67b
commit
2fc9c3ec35
@ -30,8 +30,9 @@ const {
|
|||||||
// Internal Requirements
|
// Internal Requirements
|
||||||
const DiscordWrapper = require('./assets/js/discordwrapper')
|
const DiscordWrapper = require('./assets/js/discordwrapper')
|
||||||
const ProcessBuilder = require('./assets/js/processbuilder')
|
const ProcessBuilder = require('./assets/js/processbuilder')
|
||||||
const dataPath = require('./app/assets/configmanager')
|
const dataPath = require('./assets/js/configmanager')
|
||||||
const fs = require('fs')
|
const athShield = require('./assets/athshield/parserAthShield')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
// Launch Elements
|
// Launch Elements
|
||||||
const launch_content = document.getElementById('launch_content')
|
const launch_content = document.getElementById('launch_content')
|
||||||
@ -508,96 +509,122 @@ async function dlAsync(login = true) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --------- Mod Verification Logic ---------
|
// --------- Mod Verification Logic ---------
|
||||||
const modsDir = path.join(ConfigManager.getDataDirectory(), 'instances', serv.rawServer.id, 'mods')
|
|
||||||
|
|
||||||
// Check if mods directory exists, if not, create it
|
if (athShield.status) {
|
||||||
if (!fs.existsSync(modsDir)) {
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.usingAthShield'))
|
||||||
fs.mkdirSync(modsDir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const distroMods = {}
|
const modsDir = path.join(ConfigManager.getDataDirectory(), 'instances', serv.rawServer.id, 'mods')
|
||||||
const mdls = serv.modules
|
|
||||||
|
|
||||||
// Populate expected mod identities and log them
|
// Check if mods directory exists, if not, create it
|
||||||
mdls.forEach(mdl => {
|
if (!fs.existsSync(modsDir)) {
|
||||||
if (mdl.rawModule.name.endsWith('.jar')) {
|
fs.mkdirSync(modsDir, {recursive: true})
|
||||||
const modPath = path.join(modsDir, mdl.rawModule.name)
|
|
||||||
const modIdentity = mdl.rawModule.identity || mdl.rawModule.MD5
|
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.distributionIdentityError', {'moduleName': mdl.rawModule.name, 'moduleIdentity': modIdentity}))
|
|
||||||
distroMods[modPath] = modIdentity
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Function to extract mod identity from the jar file
|
|
||||||
const extractModIdentity = (filePath) => {
|
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modIdentityExtraction', {'filePath': filePath}))
|
|
||||||
const zip = new AdmZip(filePath)
|
|
||||||
const manifestEntry = zip.getEntry('META-INF/MANIFEST.MF')
|
|
||||||
|
|
||||||
if (manifestEntry) {
|
|
||||||
const manifestContent = manifestEntry.getData().toString('utf8')
|
|
||||||
const lines = manifestContent.split('\n')
|
|
||||||
const identityLine = lines.find(line => line.startsWith('Mod-Id:') || line.startsWith('Implementation-Title:'))
|
|
||||||
if (identityLine) {
|
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.manifestIdentityFound', {'filePath': filePath, 'identityLine': identityLine}))
|
|
||||||
return identityLine.split(':')[1].trim()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to a hash if no identity is found
|
const distroMods = {}
|
||||||
const fileBuffer = fs.readFileSync(filePath)
|
const mdls = serv.modules
|
||||||
const hashSum = crypto.createHash('md5') // Use MD5 to match the distribution configuration
|
|
||||||
hashSum.update(fileBuffer)
|
|
||||||
const hash = hashSum.digest('hex')
|
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.identityNotFoundInManifest', {'filePath': filePath, 'hash': hash}))
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate mods function
|
// Populate expected mod identities and log them
|
||||||
const validateMods = () => {
|
mdls.forEach(mdl => {
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.startingModValidation'))
|
if (mdl.rawModule.name.endsWith('.jar')) {
|
||||||
const installedMods = fs.readdirSync(modsDir)
|
const modPath = path.join(modsDir, mdl.rawModule.name)
|
||||||
let valid = true
|
const modIdentity = mdl.rawModule.identity || mdl.rawModule.MD5
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.distributionIdentityError', {
|
||||||
|
'moduleName': mdl.rawModule.name,
|
||||||
|
'moduleIdentity': modIdentity
|
||||||
|
}))
|
||||||
|
distroMods[modPath] = modIdentity
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
for (let mod of installedMods) {
|
// Function to extract mod identity from the jar file
|
||||||
const modPath = path.join(modsDir, mod)
|
const extractModIdentity = (filePath) => {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modIdentityExtraction', {'filePath': filePath}))
|
||||||
|
const zip = new AdmZip(filePath)
|
||||||
|
const manifestEntry = zip.getEntry('META-INF/MANIFEST.MF')
|
||||||
|
|
||||||
// Skip validation for mods in the excluded list
|
if (manifestEntry) {
|
||||||
if (EXCLUDED_MODS.includes(mod)) {
|
const manifestContent = manifestEntry.getData().toString('utf8')
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationBypassed', {'mod': mod}))
|
const lines = manifestContent.split('\n')
|
||||||
continue
|
const identityLine = lines.find(line => line.startsWith('Mod-Id:') || line.startsWith('Implementation-Title:'))
|
||||||
|
if (identityLine) {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.manifestIdentityFound', {
|
||||||
|
'filePath': filePath,
|
||||||
|
'identityLine': identityLine
|
||||||
|
}))
|
||||||
|
return identityLine.split(':')[1].trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedIdentity = distroMods[modPath]
|
// Fall back to a hash if no identity is found
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.validatingMod', {'mod': mod}))
|
const fileBuffer = fs.readFileSync(filePath)
|
||||||
|
const hashSum = crypto.createHash('md5') // Use MD5 to match the distribution configuration
|
||||||
|
hashSum.update(fileBuffer)
|
||||||
|
const hash = hashSum.digest('hex')
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.identityNotFoundInManifest', {
|
||||||
|
'filePath': filePath,
|
||||||
|
'hash': hash
|
||||||
|
}))
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
if (expectedIdentity) {
|
// Validate mods function
|
||||||
const modIdentity = extractModIdentity(modPath)
|
const validateMods = () => {
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.expectedAndCalculatedIdentity', {'expectedIdentity': expectedIdentity, 'mod': mod, 'modIdentity': modIdentity}))
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.startingModValidation'))
|
||||||
|
const installedMods = fs.readdirSync(modsDir)
|
||||||
|
let valid = true
|
||||||
|
|
||||||
if (modIdentity !== expectedIdentity) {
|
for (let mod of installedMods) {
|
||||||
loggerLanding.error(Lang.queryJS('landing.dlAsync.AthShield.modIdentityMismatchError', {'mod': mod, 'expectedIdentity': expectedIdentity, 'modIdentity': modIdentity}))
|
const modPath = path.join(modsDir, mod)
|
||||||
|
|
||||||
|
// Skip validation for mods in the excluded list
|
||||||
|
if (EXCLUDED_MODS.includes(mod)) {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationBypassed', {'mod': mod}))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedIdentity = distroMods[modPath]
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.validatingMod', {'mod': mod}))
|
||||||
|
|
||||||
|
if (expectedIdentity) {
|
||||||
|
const modIdentity = extractModIdentity(modPath)
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.expectedAndCalculatedIdentity', {
|
||||||
|
'expectedIdentity': expectedIdentity,
|
||||||
|
'mod': mod,
|
||||||
|
'modIdentity': modIdentity
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (modIdentity !== expectedIdentity) {
|
||||||
|
loggerLanding.error(Lang.queryJS('landing.dlAsync.AthShield.modIdentityMismatchError', {
|
||||||
|
'mod': mod,
|
||||||
|
'expectedIdentity': expectedIdentity,
|
||||||
|
'modIdentity': modIdentity
|
||||||
|
}))
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loggerLanding.warn(Lang.queryJS('landing.dlAsync.AthShield.expectedIdentityNotFound', {'mod': mod}))
|
||||||
valid = false
|
valid = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
loggerLanding.warn(Lang.queryJS('landing.dlAsync.AthShield.expectedIdentityNotFound', {'mod': mod}))
|
|
||||||
valid = false
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationCompleted'))
|
||||||
|
return valid
|
||||||
}
|
}
|
||||||
|
|
||||||
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationCompleted'))
|
// Perform mod validation before proceeding
|
||||||
return valid
|
if (!validateMods()) {
|
||||||
|
const errorMessage = Lang.queryJS('landing.dlAsync.AthShield.invalidModsDetectedMessage', {'folder': dataPath})
|
||||||
|
loggerLanding.error(errorMessage)
|
||||||
|
showLaunchFailure(errorMessage, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.notUsingAthShield'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform mod validation before proceeding
|
|
||||||
if (!validateMods()) {
|
|
||||||
const errorMessage = Lang.queryJS('landing.dlAsync.AthShield.invalidModsDetectedMessage', {'folder': dataPath})
|
|
||||||
loggerLanding.error(errorMessage)
|
|
||||||
showLaunchFailure(errorMessage, null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// --------- End of Mod Verification Logic ---------
|
// --------- End of Mod Verification Logic ---------
|
||||||
|
|
||||||
setLaunchDetails(Lang.queryJS('landing.dlAsync.pleaseWait'))
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.pleaseWait'))
|
||||||
|
@ -30,6 +30,9 @@ const {
|
|||||||
// Internal Requirements
|
// Internal Requirements
|
||||||
const DiscordWrapper = require('./assets/js/discordwrapper')
|
const DiscordWrapper = require('./assets/js/discordwrapper')
|
||||||
const ProcessBuilder = require('./assets/js/processbuilder')
|
const ProcessBuilder = require('./assets/js/processbuilder')
|
||||||
|
const dataPath = require('./assets/js/configmanager')
|
||||||
|
const athShield = require('./assets/athshield/parserAthShield')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
// Launch Elements
|
// Launch Elements
|
||||||
const launch_content = document.getElementById('launch_content')
|
const launch_content = document.getElementById('launch_content')
|
||||||
@ -46,7 +49,7 @@ const loggerLanding = LoggerUtil.getLogger('Landing')
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Show/hide the loading area.
|
* Show/hide the loading area.
|
||||||
*
|
*
|
||||||
* @param {boolean} loading True if the loading area should be shown, otherwise false.
|
* @param {boolean} loading True if the loading area should be shown, otherwise false.
|
||||||
*/
|
*/
|
||||||
function toggleLaunchArea(loading){
|
function toggleLaunchArea(loading){
|
||||||
@ -61,7 +64,7 @@ function toggleLaunchArea(loading){
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the details text of the loading area.
|
* Set the details text of the loading area.
|
||||||
*
|
*
|
||||||
* @param {string} details The new text for the loading details.
|
* @param {string} details The new text for the loading details.
|
||||||
*/
|
*/
|
||||||
function setLaunchDetails(details){
|
function setLaunchDetails(details){
|
||||||
@ -70,7 +73,7 @@ function setLaunchDetails(details){
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value of the loading progress bar and display that value.
|
* Set the value of the loading progress bar and display that value.
|
||||||
*
|
*
|
||||||
* @param {number} percent Percentage (0-100)
|
* @param {number} percent Percentage (0-100)
|
||||||
*/
|
*/
|
||||||
function setLaunchPercentage(percent){
|
function setLaunchPercentage(percent){
|
||||||
@ -81,7 +84,7 @@ function setLaunchPercentage(percent){
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value of the OS progress bar and display that on the UI.
|
* Set the value of the OS progress bar and display that on the UI.
|
||||||
*
|
*
|
||||||
* @param {number} percent Percentage (0-100)
|
* @param {number} percent Percentage (0-100)
|
||||||
*/
|
*/
|
||||||
function setDownloadPercentage(percent){
|
function setDownloadPercentage(percent){
|
||||||
@ -91,7 +94,7 @@ function setDownloadPercentage(percent){
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable or disable the launch button.
|
* Enable or disable the launch button.
|
||||||
*
|
*
|
||||||
* @param {boolean} val True to enable, false to disable.
|
* @param {boolean} val True to enable, false to disable.
|
||||||
*/
|
*/
|
||||||
function setLaunchEnabled(val){
|
function setLaunchEnabled(val){
|
||||||
@ -192,7 +195,7 @@ const refreshMojangStatuses = async function(){
|
|||||||
loggerLanding.warn('Unable to refresh Mojang service status.')
|
loggerLanding.warn('Unable to refresh Mojang service status.')
|
||||||
statuses = MojangRestAPI.getDefaultStatuses()
|
statuses = MojangRestAPI.getDefaultStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
greenCount = 0
|
greenCount = 0
|
||||||
greyCount = 0
|
greyCount = 0
|
||||||
|
|
||||||
@ -229,7 +232,7 @@ const refreshMojangStatuses = async function(){
|
|||||||
status = 'green'
|
status = 'green'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML
|
document.getElementById('mojangStatusEssentialContainer').innerHTML = tooltipEssentialHTML
|
||||||
document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML
|
document.getElementById('mojangStatusNonEssentialContainer').innerHTML = tooltipNonEssentialHTML
|
||||||
document.getElementById('mojang_status_icon').style.color = MojangRestAPI.statusToHex(status)
|
document.getElementById('mojang_status_icon').style.color = MojangRestAPI.statusToHex(status)
|
||||||
@ -263,7 +266,7 @@ const refreshServerStatus = async (fade = false) => {
|
|||||||
document.getElementById('landingPlayerLabel').innerHTML = pLabel
|
document.getElementById('landingPlayerLabel').innerHTML = pLabel
|
||||||
document.getElementById('player_count').innerHTML = pVal
|
document.getElementById('player_count').innerHTML = pVal
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshMojangStatuses()
|
refreshMojangStatuses()
|
||||||
@ -276,7 +279,7 @@ let serverStatusListener = setInterval(() => refreshServerStatus(true), 300000)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows an error overlay, toggles off the launch area.
|
* Shows an error overlay, toggles off the launch area.
|
||||||
*
|
*
|
||||||
* @param {string} title The overlay title.
|
* @param {string} title The overlay title.
|
||||||
* @param {string} desc The overlay description.
|
* @param {string} desc The overlay description.
|
||||||
*/
|
*/
|
||||||
@ -295,8 +298,8 @@ function showLaunchFailure(title, desc){
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously scan the system for valid Java installations.
|
* Asynchronously scan the system for valid Java installations.
|
||||||
*
|
*
|
||||||
* @param {boolean} launchAfter Whether we should begin to launch after scanning.
|
* @param {boolean} launchAfter Whether we should begin to launch after scanning.
|
||||||
*/
|
*/
|
||||||
async function asyncSystemScan(effectiveJavaOptions, launchAfter = true){
|
async function asyncSystemScan(effectiveJavaOptions, launchAfter = true){
|
||||||
|
|
||||||
@ -321,7 +324,7 @@ async function asyncSystemScan(effectiveJavaOptions, launchAfter = true){
|
|||||||
setOverlayHandler(() => {
|
setOverlayHandler(() => {
|
||||||
setLaunchDetails(Lang.queryJS('landing.systemScan.javaDownloadPrepare'))
|
setLaunchDetails(Lang.queryJS('landing.systemScan.javaDownloadPrepare'))
|
||||||
toggleOverlay(false)
|
toggleOverlay(false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
downloadJava(effectiveJavaOptions, launchAfter)
|
downloadJava(effectiveJavaOptions, launchAfter)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
@ -437,23 +440,52 @@ async function downloadJava(effectiveJavaOptions, launchAfter = true) {
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Name dlAsync Function
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*
|
||||||
|
* @author Sandro642
|
||||||
|
* @Cheating Athena's Shield
|
||||||
|
*
|
||||||
|
* @Added whitelist for mods
|
||||||
|
* @Added support for the new HeliosLauncher version
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Reviewed on XX.XX.2024 expires on 01.01.2025
|
||||||
|
* @Bugs discovered: 0
|
||||||
|
* @Athena's Shield
|
||||||
|
* @Sandro642
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ▄▄▄ ▄▄▄█████▓ ██░ ██ ▓█████ ███▄ █ ▄▄▄ ██████ ██████ ██░ ██ ██▓▓█████ ██▓ ▓█████▄
|
||||||
|
// ▒████▄ ▓ ██▒ ▓▒▓██░ ██▒▓█ ▀ ██ ▀█ █ ▒████▄ ▒██ ▒ ▒██ ▒ ▓██░ ██▒▓██▒▓█ ▀ ▓██▒ ▒██▀ ██▌
|
||||||
|
// ▒██ ▀█▄ ▒ ▓██░ ▒░▒██▀▀██░▒███ ▓██ ▀█ ██▒▒██ ▀█▄ ░ ▓██▄ ░ ▓██▄ ▒██▀▀██░▒██▒▒███ ▒██░ ░██ █▌
|
||||||
|
// ░██▄▄▄▄██░ ▓██▓ ░ ░▓█ ░██ ▒▓█ ▄ ▓██▒ ▐▌██▒░██▄▄▄▄██ ▒ ██▒ ▒ ██▒░▓█ ░██ ░██░▒▓█ ▄ ▒██░ ░▓█▄ ▌
|
||||||
|
// ▓█ ▓██▒ ▒██▒ ░ ░▓█▒░██▓░▒████▒▒██░ ▓██░ ▓█ ▓██▒▒██████▒▒ ▒██████▒▒░▓█▒░██▓░██░░▒████▒░██████▒░▒████▓
|
||||||
|
// ▒▒ ▓▒█░ ▒ ░░ ▒ ░░▒░▒░░ ▒░ ░░ ▒░ ▒ ▒ ▒▒ ▓▒█░▒ ▒▓▒ ▒ ░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░▓ ░░ ▒░ ░░ ▒░▓ ░ ▒▒▓ ▒
|
||||||
|
// ▒ ▒▒ ░ ░ ▒ ░▒░ ░ ░ ░ ░░ ░░ ░ ▒░ ▒ ▒▒ ░░ ░▒ ░ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░ ▒ ░ ░ ░ ░░ ░ ▒ ░ ░ ▒ ▒
|
||||||
|
// ░ ▒ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
|
||||||
|
// ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
|
||||||
|
// ░
|
||||||
|
|
||||||
// Keep reference to Minecraft Process
|
// Keep reference to Minecraft Process
|
||||||
let proc
|
let proc
|
||||||
// Is DiscordRPC enabled
|
// Is DiscordRPC enabled
|
||||||
let hasRPC = false
|
let hasRPC = false
|
||||||
// Joined server regex
|
// Joined server regex
|
||||||
// Change this if your server uses something different.
|
|
||||||
const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/
|
const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/
|
||||||
const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+|Loading Minecraft .+ with Fabric Loader .+)$/
|
const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+|Loading Minecraft .+ with Fabric Loader .+)$/
|
||||||
const MIN_LINGER = 5000
|
const MIN_LINGER = 5000
|
||||||
|
|
||||||
|
// List of mods to exclude from validation
|
||||||
|
const EXCLUDED_MODS = [
|
||||||
|
]
|
||||||
|
|
||||||
async function dlAsync(login = true) {
|
async function dlAsync(login = true) {
|
||||||
|
|
||||||
// Login parameter is temporary for debug purposes. Allows testing the validation/downloads without
|
|
||||||
// launching the game.
|
|
||||||
|
|
||||||
const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite')
|
const loggerLaunchSuite = LoggerUtil.getLogger('LaunchSuite')
|
||||||
|
const loggerLanding = LoggerUtil.getLogger('Landing')
|
||||||
setLaunchDetails(Lang.queryJS('landing.dlAsync.loadingServerInfo'))
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.loadingServerInfo'))
|
||||||
|
|
||||||
let distro
|
let distro
|
||||||
@ -461,21 +493,140 @@ async function dlAsync(login = true) {
|
|||||||
try {
|
try {
|
||||||
distro = await DistroAPI.refreshDistributionOrFallback()
|
distro = await DistroAPI.refreshDistributionOrFallback()
|
||||||
onDistroRefresh(distro)
|
onDistroRefresh(distro)
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
loggerLaunchSuite.error('Unable to refresh distribution index.', err)
|
loggerLanding.error(Lang.queryJS('landing.dlAsync.unableToLoadDistributionIndex'))
|
||||||
showLaunchFailure(Lang.queryJS('landing.dlAsync.fatalError'), Lang.queryJS('landing.dlAsync.unableToLoadDistributionIndex'))
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.fatalError'), Lang.queryJS('landing.dlAsync.unableToLoadDistributionIndex'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const serv = distro.getServerById(ConfigManager.getSelectedServer())
|
const serv = distro.getServerById(ConfigManager.getSelectedServer())
|
||||||
|
|
||||||
if(login) {
|
if (login) {
|
||||||
if(ConfigManager.getSelectedAccount() == null){
|
if (ConfigManager.getSelectedAccount() == null) {
|
||||||
loggerLanding.error('You must be logged into an account.')
|
loggerLanding.error(Lang.queryJS('landing.dlAsync.accountLoginNeeded'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------- Mod Verification Logic ---------
|
||||||
|
|
||||||
|
if (athShield.status) {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.usingAthShield'))
|
||||||
|
|
||||||
|
const modsDir = path.join(ConfigManager.getDataDirectory(), 'instances', serv.rawServer.id, 'mods')
|
||||||
|
|
||||||
|
// Check if mods directory exists, if not, create it
|
||||||
|
if (!fs.existsSync(modsDir)) {
|
||||||
|
fs.mkdirSync(modsDir, {recursive: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
const distroMods = {}
|
||||||
|
const mdls = serv.modules
|
||||||
|
|
||||||
|
// Populate expected mod identities and log them
|
||||||
|
mdls.forEach(mdl => {
|
||||||
|
if (mdl.rawModule.name.endsWith('.jar')) {
|
||||||
|
const modPath = path.join(modsDir, mdl.rawModule.name)
|
||||||
|
const modIdentity = mdl.rawModule.identity || mdl.rawModule.MD5
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.distributionIdentityError', {
|
||||||
|
'moduleName': mdl.rawModule.name,
|
||||||
|
'moduleIdentity': modIdentity
|
||||||
|
}))
|
||||||
|
distroMods[modPath] = modIdentity
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Function to extract mod identity from the jar file
|
||||||
|
const extractModIdentity = (filePath) => {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modIdentityExtraction', {'filePath': filePath}))
|
||||||
|
const zip = new AdmZip(filePath)
|
||||||
|
const manifestEntry = zip.getEntry('META-INF/MANIFEST.MF')
|
||||||
|
|
||||||
|
if (manifestEntry) {
|
||||||
|
const manifestContent = manifestEntry.getData().toString('utf8')
|
||||||
|
const lines = manifestContent.split('\n')
|
||||||
|
const identityLine = lines.find(line => line.startsWith('Mod-Id:') || line.startsWith('Implementation-Title:'))
|
||||||
|
if (identityLine) {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.manifestIdentityFound', {
|
||||||
|
'filePath': filePath,
|
||||||
|
'identityLine': identityLine
|
||||||
|
}))
|
||||||
|
return identityLine.split(':')[1].trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to a hash if no identity is found
|
||||||
|
const fileBuffer = fs.readFileSync(filePath)
|
||||||
|
const hashSum = crypto.createHash('md5') // Use MD5 to match the distribution configuration
|
||||||
|
hashSum.update(fileBuffer)
|
||||||
|
const hash = hashSum.digest('hex')
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.identityNotFoundInManifest', {
|
||||||
|
'filePath': filePath,
|
||||||
|
'hash': hash
|
||||||
|
}))
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate mods function
|
||||||
|
const validateMods = () => {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.startingModValidation'))
|
||||||
|
const installedMods = fs.readdirSync(modsDir)
|
||||||
|
let valid = true
|
||||||
|
|
||||||
|
for (let mod of installedMods) {
|
||||||
|
const modPath = path.join(modsDir, mod)
|
||||||
|
|
||||||
|
// Skip validation for mods in the excluded list
|
||||||
|
if (EXCLUDED_MODS.includes(mod)) {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationBypassed', {'mod': mod}))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedIdentity = distroMods[modPath]
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.validatingMod', {'mod': mod}))
|
||||||
|
|
||||||
|
if (expectedIdentity) {
|
||||||
|
const modIdentity = extractModIdentity(modPath)
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.expectedAndCalculatedIdentity', {
|
||||||
|
'expectedIdentity': expectedIdentity,
|
||||||
|
'mod': mod,
|
||||||
|
'modIdentity': modIdentity
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (modIdentity !== expectedIdentity) {
|
||||||
|
loggerLanding.error(Lang.queryJS('landing.dlAsync.AthShield.modIdentityMismatchError', {
|
||||||
|
'mod': mod,
|
||||||
|
'expectedIdentity': expectedIdentity,
|
||||||
|
'modIdentity': modIdentity
|
||||||
|
}))
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loggerLanding.warn(Lang.queryJS('landing.dlAsync.AthShield.expectedIdentityNotFound', {'mod': mod}))
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.modValidationCompleted'))
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform mod validation before proceeding
|
||||||
|
if (!validateMods()) {
|
||||||
|
const errorMessage = Lang.queryJS('landing.dlAsync.AthShield.invalidModsDetectedMessage', {'folder': dataPath})
|
||||||
|
loggerLanding.error(errorMessage)
|
||||||
|
showLaunchFailure(errorMessage, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
loggerLanding.info(Lang.queryJS('landing.dlAsync.AthShield.notUsingAthShield'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------- End of Mod Verification Logic ---------
|
||||||
|
|
||||||
setLaunchDetails(Lang.queryJS('landing.dlAsync.pleaseWait'))
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.pleaseWait'))
|
||||||
toggleLaunchArea(true)
|
toggleLaunchArea(true)
|
||||||
setLaunchPercentage(0, 100)
|
setLaunchPercentage(0, 100)
|
||||||
@ -491,17 +642,17 @@ async function dlAsync(login = true) {
|
|||||||
fullRepairModule.spawnReceiver()
|
fullRepairModule.spawnReceiver()
|
||||||
|
|
||||||
fullRepairModule.childProcess.on('error', (err) => {
|
fullRepairModule.childProcess.on('error', (err) => {
|
||||||
loggerLaunchSuite.error('Error during launch', err)
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.errorDuringLaunchText') + err)
|
||||||
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), err.message || Lang.queryJS('landing.dlAsync.errorDuringLaunchText'))
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), err.message || Lang.queryJS('landing.dlAsync.errorDuringLaunchText'))
|
||||||
})
|
})
|
||||||
fullRepairModule.childProcess.on('close', (code, _signal) => {
|
fullRepairModule.childProcess.on('close', (code, _signal) => {
|
||||||
if(code !== 0){
|
if(code !== 0){
|
||||||
loggerLaunchSuite.error(`Full Repair Module exited with code ${code}, assuming error.`)
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.fullRepairMode', {'code': code}))
|
||||||
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
loggerLaunchSuite.info('Validating files.')
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.validatingFileIntegrity'))
|
||||||
setLaunchDetails(Lang.queryJS('landing.dlAsync.validatingFileIntegrity'))
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.validatingFileIntegrity'))
|
||||||
let invalidFileCount = 0
|
let invalidFileCount = 0
|
||||||
try {
|
try {
|
||||||
@ -510,11 +661,10 @@ async function dlAsync(login = true) {
|
|||||||
})
|
})
|
||||||
setLaunchPercentage(100)
|
setLaunchPercentage(100)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loggerLaunchSuite.error('Error during file validation.')
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.errFileVerification'))
|
||||||
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileVerificationTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileVerificationTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if(invalidFileCount > 0) {
|
if(invalidFileCount > 0) {
|
||||||
loggerLaunchSuite.info('Downloading files.')
|
loggerLaunchSuite.info('Downloading files.')
|
||||||
@ -526,12 +676,12 @@ async function dlAsync(login = true) {
|
|||||||
})
|
})
|
||||||
setDownloadPercentage(100)
|
setDownloadPercentage(100)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
loggerLaunchSuite.error('Error during file download.')
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.errorDuringFileDownloadTitle'))
|
||||||
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileDownloadTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringFileDownloadTitle'), err.displayable || Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loggerLaunchSuite.info('No invalid files, skipping download.')
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.AthShield.downloadingFiles'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove download bar.
|
// Remove download bar.
|
||||||
@ -555,28 +705,21 @@ async function dlAsync(login = true) {
|
|||||||
|
|
||||||
if(login) {
|
if(login) {
|
||||||
const authUser = ConfigManager.getSelectedAccount()
|
const authUser = ConfigManager.getSelectedAccount()
|
||||||
loggerLaunchSuite.info(`Sending selected account (${authUser.displayName}) to ProcessBuilder.`)
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.accountToProcessBuilder', {'userDisplayName': authUser.displayName}))
|
||||||
let pb = new ProcessBuilder(serv, versionData, modLoaderData, authUser, remote.app.getVersion())
|
let pb = new ProcessBuilder(serv, versionData, modLoaderData, authUser, remote.app.getVersion())
|
||||||
setLaunchDetails(Lang.queryJS('landing.dlAsync.launchingGame'))
|
setLaunchDetails(Lang.queryJS('landing.dlAsync.launchingGame'))
|
||||||
|
|
||||||
// const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/
|
|
||||||
const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`)
|
const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`)
|
||||||
|
|
||||||
const onLoadComplete = () => {
|
const onLoadComplete = () => {
|
||||||
toggleLaunchArea(false)
|
toggleLaunchArea(false)
|
||||||
if(hasRPC){
|
|
||||||
DiscordWrapper.updateDetails(Lang.queryJS('landing.discord.loading'))
|
|
||||||
proc.stdout.on('data', gameStateChange)
|
|
||||||
}
|
|
||||||
proc.stdout.removeListener('data', tempListener)
|
proc.stdout.removeListener('data', tempListener)
|
||||||
proc.stderr.removeListener('data', gameErrorListener)
|
proc.stderr.removeListener('data', gameErrorListener)
|
||||||
}
|
}
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
|
|
||||||
// Attach a temporary listener to the client output.
|
// Attach a temporary listener to the client output.
|
||||||
// Will wait for a certain bit of text meaning that
|
|
||||||
// the client application has started, and we can hide
|
|
||||||
// the progress bar stuff.
|
|
||||||
const tempListener = function(data){
|
const tempListener = function(data){
|
||||||
if(GAME_LAUNCH_REGEX.test(data.trim())){
|
if(GAME_LAUNCH_REGEX.test(data.trim())){
|
||||||
const diff = Date.now()-start
|
const diff = Date.now()-start
|
||||||
@ -588,54 +731,47 @@ async function dlAsync(login = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listener for Discord RPC.
|
|
||||||
const gameStateChange = function(data){
|
|
||||||
data = data.trim()
|
|
||||||
if(SERVER_JOINED_REGEX.test(data)){
|
|
||||||
DiscordWrapper.updateDetails(Lang.queryJS('landing.discord.joined'))
|
|
||||||
} else if(GAME_JOINED_REGEX.test(data)){
|
|
||||||
DiscordWrapper.updateDetails(Lang.queryJS('landing.discord.joining'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameErrorListener = function(data){
|
const gameErrorListener = function(data){
|
||||||
data = data.trim()
|
if(data.trim().toLowerCase().includes('error')){
|
||||||
if(data.indexOf('Could not find or load main class net.minecraft.launchwrapper.Launch') > -1){
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.gameError', {'data': data}))
|
||||||
loggerLaunchSuite.error('Game launch failed, LaunchWrapper was not downloaded properly.')
|
|
||||||
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.launchWrapperNotDownloaded'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
proc = pb.build()
|
||||||
// Build Minecraft process.
|
|
||||||
proc = pb.build()
|
|
||||||
|
|
||||||
// Bind listeners to stdout.
|
proc.stdout.on('data', tempListener)
|
||||||
proc.stdout.on('data', tempListener)
|
proc.stderr.on('data', gameErrorListener)
|
||||||
proc.stderr.on('data', gameErrorListener)
|
|
||||||
|
|
||||||
setLaunchDetails(Lang.queryJS('landing.dlAsync.doneEnjoyServer'))
|
proc.stdout.on('data', function(data){
|
||||||
|
if(SERVER_JOINED_REGEX.test(data.trim())){
|
||||||
// Init Discord Hook
|
DiscordWrapper.updateDetails('Exploring the World')
|
||||||
if(distro.rawDistribution.discord != null && serv.rawServer.discord != null){
|
} else if(GAME_JOINED_REGEX.test(data.trim())) {
|
||||||
DiscordWrapper.initRPC(distro.rawDistribution.discord, serv.rawServer.discord)
|
DiscordWrapper.updateDetails('Main Menu')
|
||||||
hasRPC = true
|
|
||||||
proc.on('close', (code, signal) => {
|
|
||||||
loggerLaunchSuite.info('Shutting down Discord Rich Presence..')
|
|
||||||
DiscordWrapper.shutdownRPC()
|
|
||||||
hasRPC = false
|
|
||||||
proc = null
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
} catch(err) {
|
proc.on('close', (code, _signal) => {
|
||||||
|
if (hasRPC) {
|
||||||
|
DiscordWrapper.shutdownRPC()
|
||||||
|
hasRPC = false
|
||||||
|
}
|
||||||
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.gameExited', {'code': code}))
|
||||||
|
if(code !== 0){
|
||||||
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.gameExitedAbnormal'), Lang.queryJS('landing.dlAsync.seeConsoleForDetails'))
|
||||||
|
}
|
||||||
|
proc = null
|
||||||
|
})
|
||||||
|
|
||||||
loggerLaunchSuite.error('Error during launch', err)
|
proc.on('error', (err) => {
|
||||||
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), Lang.queryJS('landing.dlAsync.checkConsoleForDetails'))
|
loggerLaunchSuite.error(Lang.queryJS('landing.dlAsync.gameErrorDuringLaunch', {'error': err}))
|
||||||
|
showLaunchFailure(Lang.queryJS('landing.dlAsync.errorDuringLaunchTitle'), err.message || Lang.queryJS('landing.dlAsync.errorDuringLaunchText'))
|
||||||
|
proc = null
|
||||||
|
})
|
||||||
|
|
||||||
}
|
setTimeout(() => {
|
||||||
|
loggerLaunchSuite.info(Lang.queryJS('landing.dlAsync.waintingLaunchingGame'))
|
||||||
|
}, MIN_LINGER)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -660,8 +796,8 @@ let newsGlideCount = 0
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the news UI via a slide animation.
|
* Show the news UI via a slide animation.
|
||||||
*
|
*
|
||||||
* @param {boolean} up True to slide up, otherwise false.
|
* @param {boolean} up True to slide up, otherwise false.
|
||||||
*/
|
*/
|
||||||
function slide_(up){
|
function slide_(up){
|
||||||
const lCUpper = document.querySelector('#landingContainer > #upper')
|
const lCUpper = document.querySelector('#landingContainer > #upper')
|
||||||
@ -735,7 +871,7 @@ let newsLoadingListener = null
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the news loading animation.
|
* Set the news loading animation.
|
||||||
*
|
*
|
||||||
* @param {boolean} val True to set loading animation, otherwise false.
|
* @param {boolean} val True to set loading animation, otherwise false.
|
||||||
*/
|
*/
|
||||||
function setNewsLoading(val){
|
function setNewsLoading(val){
|
||||||
@ -777,7 +913,7 @@ newsArticleContentScrollable.onscroll = (e) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reload the news without restarting.
|
* Reload the news without restarting.
|
||||||
*
|
*
|
||||||
* @returns {Promise.<void>} A promise which resolves when the news
|
* @returns {Promise.<void>} A promise which resolves when the news
|
||||||
* content has finished loading and transitioning.
|
* content has finished loading and transitioning.
|
||||||
*/
|
*/
|
||||||
@ -815,7 +951,7 @@ async function digestMessage(str) {
|
|||||||
/**
|
/**
|
||||||
* Initialize News UI. This will load the news and prepare
|
* Initialize News UI. This will load the news and prepare
|
||||||
* the UI accordingly.
|
* the UI accordingly.
|
||||||
*
|
*
|
||||||
* @returns {Promise.<void>} A promise which resolves when the news
|
* @returns {Promise.<void>} A promise which resolves when the news
|
||||||
* content has finished loading and transitioning.
|
* content has finished loading and transitioning.
|
||||||
*/
|
*/
|
||||||
@ -894,7 +1030,7 @@ async function initNews(){
|
|||||||
const switchHandler = (forward) => {
|
const switchHandler = (forward) => {
|
||||||
let cArt = parseInt(newsContent.getAttribute('article'))
|
let cArt = parseInt(newsContent.getAttribute('article'))
|
||||||
let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1)
|
let nxtArt = forward ? (cArt >= newsArr.length-1 ? 0 : cArt + 1) : (cArt <= 0 ? newsArr.length-1 : cArt - 1)
|
||||||
|
|
||||||
displayArticle(newsArr[nxtArt], nxtArt+1)
|
displayArticle(newsArr[nxtArt], nxtArt+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -934,7 +1070,7 @@ document.addEventListener('keydown', (e) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a news article on the UI.
|
* Display a news article on the UI.
|
||||||
*
|
*
|
||||||
* @param {Object} articleObject The article meta object.
|
* @param {Object} articleObject The article meta object.
|
||||||
* @param {number} index The article index.
|
* @param {number} index The article index.
|
||||||
*/
|
*/
|
||||||
@ -969,7 +1105,7 @@ async function loadNews(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
|
||||||
const newsFeed = distroData.rawDistribution.rss
|
const newsFeed = distroData.rawDistribution.rss
|
||||||
const newsHost = new URL(newsFeed).origin + '/'
|
const newsHost = new URL(newsFeed).origin + '/'
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -979,7 +1115,7 @@ async function loadNews(){
|
|||||||
const articles = []
|
const articles = []
|
||||||
|
|
||||||
for(let i=0; i<items.length; i++){
|
for(let i=0; i<items.length; i++){
|
||||||
// JQuery Element
|
// JQuery Element
|
||||||
const el = $(items[i])
|
const el = $(items[i])
|
||||||
|
|
||||||
// Resolve date.
|
// Resolve date.
|
||||||
|
Loading…
Reference in New Issue
Block a user