mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 01:53:57 +00:00
Merge branch 'feature/game-achievements' of github.com:hydralauncher/hydra into feature/cloud-sync
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
} from "@main/entity";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
@@ -17,11 +18,12 @@ export const dataSource = new DataSource({
|
||||
entities: [
|
||||
Game,
|
||||
Repack,
|
||||
UserAuth,
|
||||
UserPreferences,
|
||||
UserSubscription,
|
||||
GameShopCache,
|
||||
DownloadSource,
|
||||
DownloadQueue,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
],
|
||||
synchronize: false,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export * from "./game.entity";
|
||||
export * from "./repack.entity";
|
||||
export * from "./user-auth.entity";
|
||||
export * from "./user-preferences.entity";
|
||||
export * from "./user-subscription.entity";
|
||||
export * from "./game-shop-cache.entity";
|
||||
export * from "./game.entity";
|
||||
export * from "./game-achievements.entity";
|
||||
export * from "./download-source.entity";
|
||||
export * from "./download-queue.entity";
|
||||
export * from "./user-auth";
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
} from "typeorm";
|
||||
import { UserSubscription } from "./user-subscription.entity";
|
||||
|
||||
@Entity("user_auth")
|
||||
export class UserAuth {
|
||||
@@ -29,6 +31,9 @@ export class UserAuth {
|
||||
@Column("int", { default: 0 })
|
||||
tokenExpirationTimestamp: number;
|
||||
|
||||
@OneToOne("UserSubscription", "user")
|
||||
subscription: UserSubscription | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@@ -26,6 +26,9 @@ export class UserPreferences {
|
||||
@Column("boolean", { default: false })
|
||||
repackUpdatesNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: true })
|
||||
achievementNotificationsEnabled: boolean;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
|
||||
|
||||
42
src/main/entity/user-subscription.entity.ts
Normal file
42
src/main/entity/user-subscription.entity.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { SubscriptionStatus } from "@types";
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
} from "typeorm";
|
||||
import { UserAuth } from "./user-auth.entity";
|
||||
|
||||
@Entity("user_subscription")
|
||||
export class UserSubscription {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
subscriptionId: string;
|
||||
|
||||
@OneToOne("UserAuth", "subscription")
|
||||
@JoinColumn()
|
||||
user: UserAuth;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
status: SubscriptionStatus;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
planId: string;
|
||||
|
||||
@Column("text", { default: "" })
|
||||
planName: string;
|
||||
|
||||
@Column("datetime", { nullable: true })
|
||||
expiresAt: Date | null;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,57 +1,32 @@
|
||||
import type { GameAchievement, GameShop, UnlockedAchievement } from "@types";
|
||||
import type {
|
||||
AchievementData,
|
||||
GameShop,
|
||||
RemoteUnlockedAchievement,
|
||||
UnlockedAchievement,
|
||||
UserAchievement,
|
||||
} from "@types";
|
||||
import { registerEvent } from "../register-event";
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
userAuthRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const getAchievements = async (
|
||||
shop: string,
|
||||
objectId: string,
|
||||
userId?: string
|
||||
) => {
|
||||
const userAuth = await userAuthRepository.findOne({ where: { userId } });
|
||||
|
||||
const getAchievementLocalUser = async (shop: string, objectId: string) => {
|
||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
||||
where: { objectId, shop },
|
||||
});
|
||||
|
||||
const achievementsData = cachedAchievements?.achievements
|
||||
? JSON.parse(cachedAchievements.achievements)
|
||||
: await getGameAchievementData(objectId, shop);
|
||||
const achievementsData = await getGameAchievementData(objectId, shop);
|
||||
|
||||
if (!userId || userAuth) {
|
||||
const unlockedAchievements = JSON.parse(
|
||||
cachedAchievements?.unlockedAchievements || "[]"
|
||||
) as UnlockedAchievement[];
|
||||
|
||||
return { achievementsData, unlockedAchievements };
|
||||
}
|
||||
|
||||
const unlockedAchievements = await HydraApi.get<UnlockedAchievement[]>(
|
||||
`/users/${userId}/games/achievements`,
|
||||
{ shop, objectId, language: "en" }
|
||||
);
|
||||
|
||||
return { achievementsData, unlockedAchievements };
|
||||
};
|
||||
|
||||
export const getGameAchievements = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
userId?: string
|
||||
): Promise<GameAchievement[]> => {
|
||||
const { achievementsData, unlockedAchievements } = await getAchievements(
|
||||
shop,
|
||||
objectId,
|
||||
userId
|
||||
);
|
||||
const unlockedAchievements = JSON.parse(
|
||||
cachedAchievements?.unlockedAchievements || "[]"
|
||||
) as UnlockedAchievement[];
|
||||
|
||||
return achievementsData
|
||||
.map((achievementData) => {
|
||||
const unlockedAchiement = unlockedAchievements.find(
|
||||
const unlockedAchiementData = unlockedAchievements.find(
|
||||
(localAchievement) => {
|
||||
return (
|
||||
localAchievement.name.toUpperCase() ==
|
||||
@@ -60,29 +35,112 @@ export const getGameAchievements = async (
|
||||
}
|
||||
);
|
||||
|
||||
if (unlockedAchiement) {
|
||||
const icongray = achievementData.icongray.endsWith("/")
|
||||
? achievementData.icon
|
||||
: achievementData.icongray;
|
||||
|
||||
if (unlockedAchiementData) {
|
||||
return {
|
||||
...achievementData,
|
||||
unlocked: true,
|
||||
unlockTime: unlockedAchiement.unlockTime,
|
||||
unlockTime: unlockedAchiementData.unlockTime,
|
||||
};
|
||||
}
|
||||
|
||||
return { ...achievementData, unlocked: false, unlockTime: null };
|
||||
return {
|
||||
...achievementData,
|
||||
unlocked: false,
|
||||
unlockTime: null,
|
||||
icon: icongray,
|
||||
} as UserAchievement;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.unlocked && !b.unlocked) return -1;
|
||||
if (!a.unlocked && b.unlocked) return 1;
|
||||
return b.unlockTime - a.unlockTime;
|
||||
if (a.unlocked && b.unlocked) {
|
||||
return b.unlockTime! - a.unlockTime!;
|
||||
}
|
||||
return Number(a.hidden) - Number(b.hidden);
|
||||
});
|
||||
};
|
||||
|
||||
const getAchievementsRemoteUser = async (
|
||||
shop: string,
|
||||
objectId: string,
|
||||
userId: string
|
||||
) => {
|
||||
const userPreferences = await userPreferencesRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
const achievementsData: AchievementData[] = await getGameAchievementData(
|
||||
objectId,
|
||||
shop
|
||||
);
|
||||
|
||||
const unlockedAchievements = await HydraApi.get<RemoteUnlockedAchievement[]>(
|
||||
`/users/${userId}/games/achievements`,
|
||||
{ shop, objectId, language: userPreferences?.language || "en" }
|
||||
);
|
||||
|
||||
return achievementsData
|
||||
.map((achievementData) => {
|
||||
const unlockedAchiementData = unlockedAchievements.find(
|
||||
(localAchievement) => {
|
||||
return (
|
||||
localAchievement.name.toUpperCase() ==
|
||||
achievementData.name.toUpperCase()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const icongray = achievementData.icongray.endsWith("/")
|
||||
? achievementData.icon
|
||||
: achievementData.icongray;
|
||||
|
||||
if (unlockedAchiementData) {
|
||||
return {
|
||||
...achievementData,
|
||||
unlocked: true,
|
||||
unlockTime: unlockedAchiementData.unlockTime,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...achievementData,
|
||||
unlocked: false,
|
||||
unlockTime: null,
|
||||
icon: icongray,
|
||||
} as UserAchievement;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.unlocked && !b.unlocked) return -1;
|
||||
if (!a.unlocked && b.unlocked) return 1;
|
||||
if (a.unlocked && b.unlocked) {
|
||||
return b.unlockTime! - a.unlockTime!;
|
||||
}
|
||||
return Number(a.hidden) - Number(b.hidden);
|
||||
});
|
||||
};
|
||||
|
||||
export const getGameAchievements = async (
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
userId?: string
|
||||
): Promise<UserAchievement[]> => {
|
||||
if (!userId) {
|
||||
return getAchievementLocalUser(shop, objectId);
|
||||
}
|
||||
|
||||
return getAchievementsRemoteUser(shop, objectId, userId);
|
||||
};
|
||||
|
||||
const getGameAchievementsEvent = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
userId?: string
|
||||
): Promise<GameAchievement[]> => {
|
||||
): Promise<UserAchievement[]> => {
|
||||
return getGameAchievements(objectId, shop, userId);
|
||||
};
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ const openGameInstaller = async (
|
||||
}
|
||||
|
||||
if (fs.lstatSync(gamePath).isFile()) {
|
||||
return executeGameInstaller(gamePath);
|
||||
shell.showItemInFolder(gamePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
const setupPath = path.join(gamePath, "setup.exe");
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { HydraApi, logger } from "@main/services";
|
||||
import { ProfileVisibility, UserDetails } from "@types";
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import {
|
||||
userAuthRepository,
|
||||
userSubscriptionRepository,
|
||||
} from "@main/repository";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
|
||||
const getMe = async (
|
||||
_event: Electron.IpcMainInvokeEvent
|
||||
): Promise<UserDetails | null> => {
|
||||
return HydraApi.get<UserDetails>(`/profile/me`)
|
||||
.then(async (me) => {
|
||||
.then((me) => {
|
||||
userAuthRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
@@ -20,6 +23,23 @@ const getMe = async (
|
||||
["id"]
|
||||
);
|
||||
|
||||
if (me.subscription) {
|
||||
userSubscriptionRepository.upsert(
|
||||
{
|
||||
id: 1,
|
||||
subscriptionId: me.subscription?.id || "",
|
||||
status: me.subscription?.status || "",
|
||||
planId: me.subscription?.plan.id || "",
|
||||
planName: me.subscription?.plan.name || "",
|
||||
expiresAt: me.subscription?.expiresAt || null,
|
||||
user: { id: 1 },
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
} else {
|
||||
userSubscriptionRepository.delete({ id: 1 });
|
||||
}
|
||||
|
||||
Sentry.setUser({ id: me.id, username: me.username });
|
||||
|
||||
return me;
|
||||
@@ -28,7 +48,7 @@ const getMe = async (
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.error("Failed to get logged user", err);
|
||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||
|
||||
if (loggedUser) {
|
||||
@@ -38,6 +58,17 @@ const getMe = async (
|
||||
username: "",
|
||||
bio: "",
|
||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||
subscription: loggedUser.subscription
|
||||
? {
|
||||
id: loggedUser.subscription.subscriptionId,
|
||||
status: loggedUser.subscription.status,
|
||||
plan: {
|
||||
id: loggedUser.subscription.planId,
|
||||
name: loggedUser.subscription.planName,
|
||||
},
|
||||
expiresAt: loggedUser.subscription.expiresAt,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris
|
||||
import { app } from "electron";
|
||||
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
||||
import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement";
|
||||
import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference";
|
||||
import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription";
|
||||
|
||||
export type HydraMigration = Knex.Migration & { name: string };
|
||||
|
||||
@@ -19,6 +21,8 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
EnsureRepackUris,
|
||||
FixMissingColumns,
|
||||
CreateGameAchievement,
|
||||
AddAchievementNotificationPreference,
|
||||
CreateUserSubscription,
|
||||
]);
|
||||
}
|
||||
getMigrationName(migration: HydraMigration): string {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const AddAchievementNotificationPreference: HydraMigration = {
|
||||
name: "AddAchievementNotificationPreference",
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.boolean("achievementNotificationsEnabled").defaultTo(true);
|
||||
});
|
||||
},
|
||||
|
||||
down: (knex: Knex) => {
|
||||
return knex.schema.alterTable("user_preferences", (table) => {
|
||||
return table.dropColumn("achievementNotificationsEnabled");
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { HydraMigration } from "@main/knex-client";
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export const CreateUserSubscription: HydraMigration = {
|
||||
name: "CreateUserSubscription",
|
||||
up: async (knex: Knex) => {
|
||||
return knex.schema.createTable("user_subscription", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscriptionId").defaultTo("");
|
||||
table
|
||||
.text("userId")
|
||||
.notNullable()
|
||||
.references("user_auth.id")
|
||||
.onDelete("CASCADE");
|
||||
table.string("status").defaultTo("");
|
||||
table.string("planId").defaultTo("");
|
||||
table.string("planName").defaultTo("");
|
||||
table.dateTime("expiresAt").nullable();
|
||||
table.dateTime("createdAt").defaultTo(knex.fn.now());
|
||||
table.dateTime("updatedAt").defaultTo(knex.fn.now());
|
||||
});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {
|
||||
return knex.schema.dropTable("user_subscription");
|
||||
},
|
||||
};
|
||||
@@ -3,8 +3,8 @@ import type { Knex } from "knex";
|
||||
|
||||
export const MigrationName: HydraMigration = {
|
||||
name: "MigrationName",
|
||||
up: async (knex: Knex) => {
|
||||
await knex.schema.createTable("table_name", (table) => {});
|
||||
up: (knex: Knex) => {
|
||||
return knex.schema.createTable("table_name", async (table) => {});
|
||||
},
|
||||
|
||||
down: async (knex: Knex) => {},
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UserPreferences,
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
} from "@main/entity";
|
||||
|
||||
export const gameRepository = dataSource.getRepository(Game);
|
||||
@@ -26,5 +27,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||
|
||||
export const userSubscriptionRepository =
|
||||
dataSource.getRepository(UserSubscription);
|
||||
|
||||
export const gameAchievementRepository =
|
||||
dataSource.getRepository(GameAchievement);
|
||||
|
||||
@@ -113,8 +113,8 @@ const compareFile = async (game: Game, file: AchievementFile) => {
|
||||
logger.log(
|
||||
"Detected change in file",
|
||||
file.filePath,
|
||||
currentStat.mtimeMs,
|
||||
fileStats.get(file.filePath)
|
||||
previousStat,
|
||||
currentStat.mtimeMs
|
||||
);
|
||||
await processAchievementFileDiff(game, file);
|
||||
} catch (err) {
|
||||
|
||||
@@ -53,9 +53,13 @@ const getPathFromCracker = (cracker: Cracker) => {
|
||||
if (cracker === Cracker.onlineFix) {
|
||||
return [
|
||||
{
|
||||
folderPath: path.join(publicDocuments, Cracker.onlineFix),
|
||||
folderPath: path.join(publicDocuments, "OnlineFix"),
|
||||
fileLocation: ["Stats", "Achievements.ini"],
|
||||
},
|
||||
{
|
||||
folderPath: path.join(publicDocuments, "OnlineFix"),
|
||||
fileLocation: ["Achievements.ini"],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import {
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { AchievementData } from "@types";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export const getGameAchievementData = async (
|
||||
objectId: string,
|
||||
@@ -12,13 +15,13 @@ export const getGameAchievementData = async (
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
return HydraApi.get("/games/achievements", {
|
||||
return HydraApi.get<AchievementData[]>("/games/achievements", {
|
||||
shop,
|
||||
objectId,
|
||||
language: userPreferences?.language || "en",
|
||||
})
|
||||
.then(async (achievements) => {
|
||||
await gameAchievementRepository.upsert(
|
||||
.then((achievements) => {
|
||||
gameAchievementRepository.upsert(
|
||||
{
|
||||
objectId,
|
||||
shop,
|
||||
@@ -29,5 +32,17 @@ export const getGameAchievementData = async (
|
||||
|
||||
return achievements;
|
||||
})
|
||||
.catch(() => []);
|
||||
.catch((err) => {
|
||||
if (err instanceof UserNotLoggedInError) {
|
||||
throw err;
|
||||
}
|
||||
logger.error("Failed to get game achievements", err);
|
||||
return gameAchievementRepository
|
||||
.findOne({
|
||||
where: { objectId, shop },
|
||||
})
|
||||
.then((gameAchievements) => {
|
||||
return JSON.parse(gameAchievements?.achievements || "[]");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
||||
import type { GameShop, UnlockedAchievement } from "@types";
|
||||
import {
|
||||
gameAchievementRepository,
|
||||
gameRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import type { AchievementData, GameShop, UnlockedAchievement } from "@types";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { HydraApi } from "../hydra-api";
|
||||
import { getGameAchievements } from "@main/events/catalogue/get-game-achievements";
|
||||
@@ -18,11 +22,15 @@ const saveAchievementsOnLocal = async (
|
||||
},
|
||||
["objectId", "shop"]
|
||||
)
|
||||
.then(async () => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
await getGameAchievements(objectId, shop as GameShop)
|
||||
);
|
||||
.then(() => {
|
||||
return getGameAchievements(objectId, shop as GameShop)
|
||||
.then((achievements) => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-update-achievements-${objectId}-${shop}`,
|
||||
achievements
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -38,16 +46,23 @@ export const mergeAchievements = async (
|
||||
|
||||
if (!game) return;
|
||||
|
||||
const localGameAchievement = await gameAchievementRepository.findOne({
|
||||
where: {
|
||||
objectId,
|
||||
shop,
|
||||
},
|
||||
});
|
||||
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||
gameAchievementRepository.findOne({
|
||||
where: {
|
||||
objectId,
|
||||
shop,
|
||||
},
|
||||
}),
|
||||
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
||||
]);
|
||||
|
||||
const achievementsData = JSON.parse(
|
||||
localGameAchievement?.achievements || "[]"
|
||||
) as AchievementData[];
|
||||
|
||||
const unlockedAchievements = JSON.parse(
|
||||
localGameAchievement?.unlockedAchievements || "[]"
|
||||
).filter((achievement) => achievement.name);
|
||||
).filter((achievement) => achievement.name) as UnlockedAchievement[];
|
||||
|
||||
const newAchievements = achievements
|
||||
.filter((achievement) => {
|
||||
@@ -64,26 +79,28 @@ export const mergeAchievements = async (
|
||||
};
|
||||
});
|
||||
|
||||
if (newAchievements.length && publishNotification) {
|
||||
if (
|
||||
newAchievements.length &&
|
||||
publishNotification &&
|
||||
userPreferences?.achievementNotificationsEnabled
|
||||
) {
|
||||
const achievementsInfo = newAchievements
|
||||
.sort((a, b) => {
|
||||
return a.unlockTime - b.unlockTime;
|
||||
})
|
||||
.map((achievement) => {
|
||||
return JSON.parse(localGameAchievement?.achievements || "[]").find(
|
||||
(steamAchievement) => {
|
||||
return (
|
||||
achievement.name.toUpperCase() ===
|
||||
steamAchievement.name.toUpperCase()
|
||||
);
|
||||
}
|
||||
);
|
||||
return achievementsData.find((steamAchievement) => {
|
||||
return (
|
||||
achievement.name.toUpperCase() ===
|
||||
steamAchievement.name.toUpperCase()
|
||||
);
|
||||
});
|
||||
})
|
||||
.filter((achievement) => achievement)
|
||||
.map((achievement) => {
|
||||
return {
|
||||
displayName: achievement.displayName,
|
||||
iconUrl: achievement.icon,
|
||||
displayName: achievement!.displayName,
|
||||
iconUrl: achievement!.icon,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -98,10 +115,14 @@ export const mergeAchievements = async (
|
||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||
|
||||
if (game?.remoteId) {
|
||||
return HydraApi.put("/profile/games/achievements", {
|
||||
id: game.remoteId,
|
||||
achievements: mergedLocalAchievements,
|
||||
})
|
||||
return HydraApi.put(
|
||||
"/profile/games/achievements",
|
||||
{
|
||||
id: game.remoteId,
|
||||
achievements: mergedLocalAchievements,
|
||||
},
|
||||
{ needsCloud: true }
|
||||
)
|
||||
.then((response) => {
|
||||
return saveAchievementsOnLocal(
|
||||
response.objectId,
|
||||
|
||||
@@ -73,7 +73,12 @@ export const parseAchievementFile = (
|
||||
|
||||
const iniParse = (filePath: string) => {
|
||||
try {
|
||||
const lines = readFileSync(filePath, "utf-8").split(/[\r\n]+/);
|
||||
const fileContent = readFileSync(filePath, "utf-8");
|
||||
|
||||
const lines =
|
||||
fileContent.charCodeAt(0) === 0xfeff
|
||||
? fileContent.slice(1).split(/[\r\n]+/)
|
||||
: fileContent.split(/[\r\n]+/);
|
||||
|
||||
let objectName = "";
|
||||
const object: Record<string, Record<string, string | number>> = {};
|
||||
@@ -112,11 +117,21 @@ const processOnlineFix = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.achieved) {
|
||||
if (unlockedAchievement?.achieved == "true") {
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.timestamp * 1000,
|
||||
});
|
||||
} else if (unlockedAchievement?.Achieved == "true") {
|
||||
const unlockTime = unlockedAchievement.TimeUnlocked;
|
||||
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime:
|
||||
unlockTime.length === 7
|
||||
? unlockTime * 1000 * 1000
|
||||
: unlockTime * 1000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +144,7 @@ const processCreamAPI = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.achieved) {
|
||||
if (unlockedAchievement?.achieved == "true") {
|
||||
const unlockTime = unlockedAchievement.unlocktime;
|
||||
parsedUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
@@ -207,7 +222,7 @@ const processDefault = (unlockedAchievements: any): UnlockedAchievement[] => {
|
||||
for (const achievement of Object.keys(unlockedAchievements)) {
|
||||
const unlockedAchievement = unlockedAchievements[achievement];
|
||||
|
||||
if (unlockedAchievement?.Achieved) {
|
||||
if (unlockedAchievement?.Achieved == "1") {
|
||||
newUnlockedAchievements.push({
|
||||
name: achievement,
|
||||
unlockTime: unlockedAchievement.UnlockTime * 1000,
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { userAuthRepository } from "@main/repository";
|
||||
import {
|
||||
userAuthRepository,
|
||||
userSubscriptionRepository,
|
||||
} from "@main/repository";
|
||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import url from "url";
|
||||
import { uploadGamesBatch } from "./library-sync";
|
||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||
import { logger } from "./logger";
|
||||
import { UserNotLoggedInError } from "@shared";
|
||||
import {
|
||||
UserNotLoggedInError,
|
||||
UserWithoutCloudSubscriptionError,
|
||||
} from "@shared";
|
||||
import { omit } from "lodash-es";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
interface HydraApiOptions {
|
||||
needsAuth: boolean;
|
||||
needsAuth?: boolean;
|
||||
needsCloud?: boolean;
|
||||
}
|
||||
|
||||
export class HydraApi {
|
||||
@@ -31,6 +38,19 @@ export class HydraApi {
|
||||
return this.userAuth.authToken !== "";
|
||||
}
|
||||
|
||||
private static async hasCloudSubscription() {
|
||||
// TODO change this later, this is just a quick test
|
||||
return userSubscriptionRepository
|
||||
.findOne({ where: { id: 1 } })
|
||||
.then((userSubscription) => {
|
||||
if (userSubscription?.status !== "active") return false;
|
||||
return (
|
||||
!userSubscription.expiresAt ||
|
||||
userSubscription!.expiresAt > new Date()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static async handleExternalAuth(uri: string) {
|
||||
const { payload } = url.parse(uri, true).query;
|
||||
|
||||
@@ -234,15 +254,28 @@ export class HydraApi {
|
||||
throw err;
|
||||
};
|
||||
|
||||
private static async validateOptions(options?: HydraApiOptions) {
|
||||
const needsAuth = options?.needsAuth == undefined || options.needsAuth;
|
||||
const needsCloud = options?.needsCloud === true;
|
||||
|
||||
if (needsAuth) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
}
|
||||
|
||||
if (needsCloud) {
|
||||
if (!(await this.hasCloudSubscription())) {
|
||||
throw new UserWithoutCloudSubscriptionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async get<T = any>(
|
||||
url: string,
|
||||
params?: any,
|
||||
options?: HydraApiOptions
|
||||
) {
|
||||
if (!options || options.needsAuth) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
}
|
||||
await this.validateOptions(options);
|
||||
|
||||
return this.instance
|
||||
.get<T>(url, { params, ...this.getAxiosConfig() })
|
||||
@@ -255,10 +288,7 @@ export class HydraApi {
|
||||
data?: any,
|
||||
options?: HydraApiOptions
|
||||
) {
|
||||
if (!options || options.needsAuth) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
}
|
||||
await this.validateOptions(options);
|
||||
|
||||
return this.instance
|
||||
.post<T>(url, data, this.getAxiosConfig())
|
||||
@@ -271,10 +301,7 @@ export class HydraApi {
|
||||
data?: any,
|
||||
options?: HydraApiOptions
|
||||
) {
|
||||
if (!options || options.needsAuth) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
}
|
||||
await this.validateOptions(options);
|
||||
|
||||
return this.instance
|
||||
.put<T>(url, data, this.getAxiosConfig())
|
||||
@@ -287,10 +314,7 @@ export class HydraApi {
|
||||
data?: any,
|
||||
options?: HydraApiOptions
|
||||
) {
|
||||
if (!options || options.needsAuth) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
}
|
||||
await this.validateOptions(options);
|
||||
|
||||
return this.instance
|
||||
.patch<T>(url, data, this.getAxiosConfig())
|
||||
@@ -299,10 +323,7 @@ export class HydraApi {
|
||||
}
|
||||
|
||||
static async delete<T = any>(url: string, options?: HydraApiOptions) {
|
||||
if (!options || options.needsAuth) {
|
||||
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
|
||||
await this.revalidateAccessTokenIfExpired();
|
||||
}
|
||||
await this.validateOptions(options);
|
||||
|
||||
return this.instance
|
||||
.delete<T>(url, this.getAxiosConfig())
|
||||
|
||||
Reference in New Issue
Block a user