ci: updating build to support ws url

This commit is contained in:
Chubby Granny Chaser
2025-05-09 20:52:03 +01:00
parent aa18b57ada
commit 6c44cc0cc4
83 changed files with 1810 additions and 3040 deletions

View File

@@ -1,26 +1,26 @@
import path from "node:path";
import fs from "node:fs";
import { app } from "electron";
import type { Game, AchievementFile } from "@types";
import { Cracker } from "@shared";
import { achievementsLogger } from "../logger";
import { SystemPath } from "../system-path";
const getAppDataPath = () => {
if (process.platform === "win32") {
return app.getPath("appData");
return SystemPath.getPath("appData");
}
const user = app.getPath("home").split("/").pop();
const user = SystemPath.getPath("home").split("/").pop();
return path.join("drive_c", "users", user || "", "AppData", "Roaming");
};
const getDocumentsPath = () => {
if (process.platform === "win32") {
return app.getPath("documents");
return SystemPath.getPath("documents");
}
const user = app.getPath("home").split("/").pop();
const user = SystemPath.getPath("home").split("/").pop();
return path.join("drive_c", "users", user || "", "Documents");
};
@@ -38,7 +38,7 @@ const getLocalAppDataPath = () => {
return path.join(appData, "..", "Local");
}
const user = app.getPath("home").split("/").pop();
const user = SystemPath.getPath("home").split("/").pop();
return path.join("drive_c", "users", user || "", "AppData", "Local");
};

View File

@@ -25,7 +25,7 @@ export const getGameAchievementData = async (
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
valueEncoding: "utf8",
})
.then((language) => language || "en");

View File

@@ -208,6 +208,19 @@ const processSkidrow = (unlockedAchievements: any): UnlockedAchievement[] => {
const processGoldberg = (unlockedAchievements: any): UnlockedAchievement[] => {
const newUnlockedAchievements: UnlockedAchievement[] = [];
if (Array.isArray(unlockedAchievements)) {
for (const achievement of unlockedAchievements) {
if (achievement?.earned) {
newUnlockedAchievements.push({
name: achievement.name,
unlockTime: achievement.earned_time * 1000,
});
}
}
return newUnlockedAchievements;
}
for (const achievement of Object.keys(unlockedAchievements)) {
const unlockedAchievement = unlockedAchievements[achievement];

View File

@@ -1,5 +1,4 @@
import { levelKeys, gamesSublevel, db } from "@main/level";
import { app } from "electron";
import path from "node:path";
import * as tar from "tar";
import crypto from "node:crypto";
@@ -15,6 +14,7 @@ import axios from "axios";
import { Ludusavi } from "./ludusavi";
import { formatDate, SubscriptionRequiredError } from "@shared";
import i18next, { t } from "i18next";
import { SystemPath } from "./system-path";
export class CloudSync {
public static getBackupLabel(automatic: boolean) {
@@ -102,7 +102,7 @@ export class CloudSync {
shop,
objectId,
hostname: os.hostname(),
homeDir: normalizePath(app.getPath("home")),
homeDir: normalizePath(SystemPath.getPath("home")),
downloadOptionTitle,
platform: os.platform(),
label,

View File

@@ -4,24 +4,21 @@ import fs from "node:fs";
import cp from "node:child_process";
import path from "node:path";
import { logger } from "./logger";
import { app } from "electron";
import { WindowManager } from "./window-manager";
import { SystemPath } from "./system-path";
export class CommonRedistManager {
private static readonly redistributables = [
"dotNetFx40_Full_setup.exe",
"dxwebsetup.exe",
"directx_Jun2010_redist.exe",
"oalinst.exe",
"install.bat",
"vcredist_2015-2019_x64.exe",
"vcredist_2015-2019_x86.exe",
"vcredist_x64.exe",
"vcredist_x86.exe",
"xnafx40_redist.msi",
"VisualCppRedist_AIO_x86_x64.exe",
];
private static readonly installationTimeout = 1000 * 60 * 5; // 5 minutes
private static readonly installationLog = path.join(
app.getPath("temp"),
SystemPath.getPath("temp"),
"common_redist_install.log"
);
@@ -47,6 +44,8 @@ export class CommonRedistManager {
fs.readFile(this.installationLog, "utf-8", (err, data) => {
if (err) return logger.error("Error reading log file:", err);
logger.log("Redist log file updated:", data);
const tail = data.split("\n").at(-2)?.trim();
if (tail?.includes(installationCompleteMessage)) {
@@ -92,7 +91,7 @@ export class CommonRedistManager {
for (const redist of this.redistributables) {
const filePath = path.join(commonRedistPath, redist);
if (fs.existsSync(filePath)) {
if (fs.existsSync(filePath) && redist !== "install.bat") {
continue;
}

View File

@@ -11,6 +11,7 @@ import { getUserData } from "./user/get-user-data";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels";
import type { Auth, User } from "@types";
import { WSManager } from "./ws-manager";
interface HydraApiOptions {
needsAuth?: boolean;
@@ -101,6 +102,8 @@ export class HydraApi {
WindowManager.mainWindow.webContents.send("on-signin");
await clearGamesRemoteIds();
uploadGamesBatch();
WSManager.close();
WSManager.connect();
}
}

View File

@@ -5,7 +5,7 @@ import { gamesSublevel, levelKeys } from "@main/level";
export const createGame = async (game: Game) => {
return HydraApi.post(`/profile/games`, {
objectId: game.objectId,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds ?? 0),
shop: game.shop,
lastTimePlayed: game.lastTimePlayed,
}).then((response) => {

View File

@@ -8,9 +8,13 @@ import YAML from "yaml";
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
import { SystemPath } from "./system-path";
export class Ludusavi {
private static ludusaviPath = path.join(app.getPath("appData"), "ludusavi");
private static ludusaviPath = path.join(
SystemPath.getPath("appData"),
"ludusavi"
);
private static ludusaviConfigPath = path.join(
this.ludusaviPath,
"config.yaml"

View File

@@ -3,7 +3,6 @@ import { DownloadManager } from "./download";
import { watchProcesses } from "./process-watcher";
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
import { UpdateManager } from "./update-manager";
import { watchFriendRequests } from "@main/events/profile/sync-friend-requests";
import { MAIN_LOOP_INTERVAL } from "@main/constants";
export const startMainLoop = async () => {
@@ -11,7 +10,6 @@ export const startMainLoop = async () => {
while (true) {
await Promise.allSettled([
watchProcesses(),
watchFriendRequests(),
DownloadManager.watchDownloads(),
AchievementWatcherManager.watchAchievements(),
DownloadManager.getSeedStatus(),

View File

@@ -1,4 +1,4 @@
import { Notification, app } from "electron";
import { Notification } from "electron";
import { t } from "i18next";
import trayIcon from "@resources/tray-icon.png?asset";
import fs from "node:fs";
@@ -13,13 +13,14 @@ import { WindowManager } from "../window-manager";
import type { Game, UserPreferences } from "@types";
import { db, levelKeys } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
import { SystemPath } from "../system-path";
async function downloadImage(url: string | null) {
if (!url) return undefined;
if (!url.startsWith("http")) return undefined;
const fileName = url.split("/").pop()!;
const outputPath = path.join(app.getPath("temp"), fileName);
const outputPath = path.join(SystemPath.getPath("temp"), fileName);
const writer = fs.createWriteStream(outputPath);
const response = await axios.get(url, {
@@ -80,7 +81,9 @@ export const publishNotificationUpdateReadyToInstall = async (
.show();
};
export const publishNewFriendRequestNotification = async () => {
export const publishNewFriendRequestNotification = async (
senderProfileImageUrl?: string
) => {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
@@ -97,7 +100,9 @@ export const publishNewFriendRequestNotification = async () => {
body: t("new_friend_request_description", {
ns: "notifications",
}),
icon: trayIcon,
icon: senderProfileImageUrl
? await downloadImage(senderProfileImageUrl)
: trayIcon,
}).show();
};

View File

@@ -7,7 +7,8 @@ import crypto from "node:crypto";
import { pythonRpcLogger } from "./logger";
import { Readable } from "node:stream";
import { app, dialog } from "electron";
import { app, dialog, safeStorage } from "electron";
import { db, levelKeys } from "@main/level";
interface GamePayload {
game_id: string;
@@ -30,17 +31,12 @@ const rustBinaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
export class PythonRPC {
public static readonly BITTORRENT_PORT = "5881";
public static readonly RPC_PORT = "8084";
private static readonly RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
private static pythonProcess: cp.ChildProcess | null = null;
public static readonly rpc = axios.create({
baseURL: `http://localhost:${this.RPC_PORT}`,
headers: {
"x-hydra-rpc-password": this.RPC_PASSWORD,
},
});
private static pythonProcess: cp.ChildProcess | null = null;
private static logStderr(readable: Readable | null) {
if (!readable) return;
@@ -48,14 +44,37 @@ export class PythonRPC {
readable.on("data", pythonRpcLogger.log);
}
public static spawn(
private static async getRPCPassword() {
const existingPassword = await db.get(levelKeys.rpcPassword, {
valueEncoding: "utf8",
});
if (existingPassword)
return safeStorage.decryptString(Buffer.from(existingPassword, "hex"));
const newPassword = crypto.randomBytes(32).toString("hex");
await db.put(
levelKeys.rpcPassword,
safeStorage.encryptString(newPassword).toString("hex"),
{
valueEncoding: "utf8",
}
);
return newPassword;
}
public static async spawn(
initialDownload?: GamePayload,
initialSeeding?: GamePayload[]
) {
const rpcPassword = await this.getRPCPassword();
const commonArgs = [
this.BITTORRENT_PORT,
this.RPC_PORT,
this.RPC_PASSWORD,
rpcPassword,
initialDownload ? JSON.stringify(initialDownload) : "",
initialSeeding ? JSON.stringify(initialSeeding) : "",
app.isPackaged
@@ -116,6 +135,8 @@ export class PythonRPC {
this.pythonProcess = childProcess;
}
this.rpc.defaults.headers.common["x-hydra-rpc-password"] = rpcPassword;
}
public static kill() {

View File

@@ -0,0 +1,45 @@
import { app, dialog } from "electron";
import { logger } from "./logger";
export class SystemPath {
static readonly paths = {
userData: "userData",
downloads: "downloads",
documents: "documents",
desktop: "desktop",
home: "home",
appData: "appData",
temp: "temp",
};
static checkIfPathsAreAvailable() {
const paths = Object.keys(SystemPath.paths) as Array<
keyof typeof SystemPath.paths
>;
paths.forEach((pathName) => {
try {
app.getPath(pathName);
} catch (error) {
logger.error(`Error getting path ${pathName}`);
if (error instanceof Error) {
logger.error(error.message, error.stack);
}
dialog.showErrorBox(
`Hydra was not able to find path for '${pathName}' system folder`,
`Some functionalities may not work as expected.\nPlease check your system settings.`
);
}
});
}
static getPath(pathName: keyof typeof SystemPath.paths): string {
try {
return app.getPath(pathName);
} catch (error) {
logger.error(`Error getting path: ${error}`);
return "";
}
}
}

View File

@@ -16,7 +16,7 @@ import trayIcon from "@resources/tray-icon.png?asset";
import { HydraApi } from "./hydra-api";
import UserAgent from "user-agents";
import { db, gamesSublevel, levelKeys } from "@main/level";
import { slice, sortBy } from "lodash-es";
import { orderBy, slice } from "lodash-es";
import type { ScreenState, UserPreferences } from "@types";
import { AuthPage } from "@shared";
import { isStaging } from "@main/constants";
@@ -370,14 +370,14 @@ export class WindowManager {
!game.isDeleted && game.executablePath && game.lastTimePlayed
);
const sortedGames = sortBy(filteredGames, "lastTimePlayed", "DESC");
const sortedGames = orderBy(filteredGames, "lastTimePlayed", "desc");
return slice(sortedGames, 5);
return slice(sortedGames, 0, 6);
});
const recentlyPlayedGames: Array<MenuItemConstructorOptions | MenuItem> =
games.map(({ title, executablePath }) => ({
label: title.length > 15 ? `${title.slice(0, 15)}` : title,
label: title.length > 18 ? `${title.slice(0, 18)}` : title,
type: "normal",
click: async () => {
if (!executablePath) return;
@@ -418,7 +418,10 @@ export class WindowManager {
},
]);
tray.setContextMenu(contextMenu);
if (process.platform === "linux") {
tray.setContextMenu(contextMenu);
}
return contextMenu;
};

View File

@@ -1,14 +1,19 @@
import { WebSocket } from "ws";
import { HydraApi } from "./hydra-api";
import { Envelope } from "@main/generated/envelope";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
export class WSManager {
private static ws: WebSocket;
private static ws: WebSocket | null = null;
private static reconnectInterval = 1000;
private static maxReconnectInterval = 30000;
private static reconnectAttempts = 0;
private static reconnecting = false;
static async connect() {
const { token } = await HydraApi.post<{ token: string }>("/auth/ws");
console.log("WS TOKEN", token);
this.ws = new WebSocket(import.meta.env.MAIN_VITE_WS_URL, {
headers: {
Authorization: `Bearer ${token}`,
@@ -16,15 +21,55 @@ export class WSManager {
});
this.ws.on("open", () => {
console.log("open");
});
this.ws.on("error", (error) => {
console.error(error);
logger.info("WS connected");
this.reconnectInterval = 1000;
this.reconnecting = false;
});
this.ws.on("message", (message) => {
console.log(message);
const envelope = Envelope.fromBinary(
new Uint8Array(Buffer.from(message.toString()))
);
if (envelope.payload.oneofKind === "friendRequest") {
WindowManager.mainWindow?.webContents.send("on-sync-friend-requests", {
friendRequestCount: envelope.payload.friendRequest.friendRequestCount,
});
}
});
this.ws.on("close", () => {
logger.warn("WS closed. Attempting reconnect...");
this.tryReconnect();
});
this.ws.on("error", (err) => {
logger.error("WS error:", err);
this.tryReconnect();
});
}
private static async tryReconnect() {
if (this.reconnecting) return;
this.reconnecting = true;
this.reconnectAttempts++;
const waitTime = Math.min(
this.reconnectInterval * 2 ** this.reconnectAttempts,
this.maxReconnectInterval
);
logger.info(`Reconnecting in ${waitTime / 1000}s...`);
setTimeout(() => {
this.connect();
}, waitTime);
}
public static async close() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}