feat: adding new friend session notification

This commit is contained in:
Chubby Granny Chaser
2025-05-10 17:43:09 +01:00
parent fee9cfb3e8
commit 216f813771
15 changed files with 286 additions and 414 deletions

View File

@@ -1,5 +1,10 @@
import { registerEvent } from "../register-event";
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
import {
DownloadManager,
HydraApi,
WSClient,
gamesPlaytime,
} from "@main/services";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
@@ -30,6 +35,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
databaseOperations,
HydraApi.post("/auth/logout").catch(() => {}),
]);
WSClient.close();
};
registerEvent("signOut", signOut);

View File

@@ -14,348 +14,227 @@ import { MessageType } from "@protobuf-ts/runtime";
* @generated from protobuf message FriendRequest
*/
export interface FriendRequest {
/**
* @generated from protobuf field: int32 friend_request_count = 1;
*/
friendRequestCount: number;
/**
* @generated from protobuf field: optional string sender_id = 2;
*/
senderId?: string;
/**
* @generated from protobuf field: int32 friend_request_count = 1;
*/
friendRequestCount: number;
/**
* @generated from protobuf field: optional string sender_id = 2;
*/
senderId?: string;
}
/**
* @generated from protobuf message UpdateGamePlaytime
* @generated from protobuf message FriendGameSession
*/
export interface UpdateGamePlaytime {
/**
* @generated from protobuf field: int64 playtime_delta_in_seconds = 1;
*/
playtimeDeltaInSeconds: bigint;
/**
* @generated from protobuf field: string last_time_played = 2;
*/
lastTimePlayed: string;
/**
* @generated from protobuf field: string game_id = 3;
*/
gameId: string;
export interface FriendGameSession {
/**
* @generated from protobuf field: string object_id = 1;
*/
objectId: string;
/**
* @generated from protobuf field: string shop = 2;
*/
shop: string;
/**
* @generated from protobuf field: string friend_id = 3;
*/
friendId: string;
}
/**
* @generated from protobuf message Envelope
*/
export interface Envelope {
/**
* @generated from protobuf oneof: payload
*/
payload:
| {
/**
* @generated from protobuf oneof: payload
*/
payload: {
oneofKind: "friendRequest";
/**
* @generated from protobuf field: FriendRequest friend_request = 1;
*/
friendRequest: FriendRequest;
}
| {
oneofKind: "updateGamePlaytime";
} | {
oneofKind: "friendGameSession";
/**
* @generated from protobuf field: UpdateGamePlaytime update_game_playtime = 2;
* @generated from protobuf field: FriendGameSession friend_game_session = 2;
*/
updateGamePlaytime: UpdateGamePlaytime;
}
| {
friendGameSession: FriendGameSession;
} | {
oneofKind: undefined;
};
};
}
// @generated message type with reflection information, may provide speed optimized methods
class FriendRequest$Type extends MessageType<FriendRequest> {
constructor() {
super("FriendRequest", [
{
no: 1,
name: "friend_request_count",
kind: "scalar",
T: 5 /*ScalarType.INT32*/,
},
{
no: 2,
name: "sender_id",
kind: "scalar",
opt: true,
T: 9 /*ScalarType.STRING*/,
},
]);
}
create(value?: PartialMessage<FriendRequest>): FriendRequest {
const message = globalThis.Object.create(this.messagePrototype!);
message.friendRequestCount = 0;
if (value !== undefined)
reflectionMergePartial<FriendRequest>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: FriendRequest
): FriendRequest {
const message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
const [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 friend_request_count */ 1:
message.friendRequestCount = reader.int32();
break;
case /* optional string sender_id */ 2:
message.senderId = reader.string();
break;
default:
const u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
const d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
constructor() {
super("FriendRequest", [
{ no: 1, name: "friend_request_count", kind: "scalar", T: 5 /*ScalarType.INT32*/ },
{ no: 2, name: "sender_id", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<FriendRequest>): FriendRequest {
const message = globalThis.Object.create((this.messagePrototype!));
message.friendRequestCount = 0;
if (value !== undefined)
reflectionMergePartial<FriendRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FriendRequest): FriendRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 friend_request_count */ 1:
message.friendRequestCount = reader.int32();
break;
case /* optional string sender_id */ 2:
message.senderId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FriendRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* int32 friend_request_count = 1; */
if (message.friendRequestCount !== 0)
writer.tag(1, WireType.Varint).int32(message.friendRequestCount);
/* optional string sender_id = 2; */
if (message.senderId !== undefined)
writer.tag(2, WireType.LengthDelimited).string(message.senderId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
return message;
}
internalBinaryWrite(
message: FriendRequest,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* int32 friend_request_count = 1; */
if (message.friendRequestCount !== 0)
writer.tag(1, WireType.Varint).int32(message.friendRequestCount);
/* optional string sender_id = 2; */
if (message.senderId !== undefined)
writer.tag(2, WireType.LengthDelimited).string(message.senderId);
const u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message FriendRequest
*/
export const FriendRequest = new FriendRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class UpdateGamePlaytime$Type extends MessageType<UpdateGamePlaytime> {
constructor() {
super("UpdateGamePlaytime", [
{
no: 1,
name: "playtime_delta_in_seconds",
kind: "scalar",
T: 3 /*ScalarType.INT64*/,
L: 0 /*LongType.BIGINT*/,
},
{
no: 2,
name: "last_time_played",
kind: "scalar",
T: 9 /*ScalarType.STRING*/,
},
{ no: 3, name: "game_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
]);
}
create(value?: PartialMessage<UpdateGamePlaytime>): UpdateGamePlaytime {
const message = globalThis.Object.create(this.messagePrototype!);
message.playtimeDeltaInSeconds = 0n;
message.lastTimePlayed = "";
message.gameId = "";
if (value !== undefined)
reflectionMergePartial<UpdateGamePlaytime>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: UpdateGamePlaytime
): UpdateGamePlaytime {
const message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
const [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int64 playtime_delta_in_seconds */ 1:
message.playtimeDeltaInSeconds = reader.int64().toBigInt();
break;
case /* string last_time_played */ 2:
message.lastTimePlayed = reader.string();
break;
case /* string game_id */ 3:
message.gameId = reader.string();
break;
default:
const u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
const d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
class FriendGameSession$Type extends MessageType<FriendGameSession> {
constructor() {
super("FriendGameSession", [
{ no: 1, name: "object_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "shop", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "friend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<FriendGameSession>): FriendGameSession {
const message = globalThis.Object.create((this.messagePrototype!));
message.objectId = "";
message.shop = "";
message.friendId = "";
if (value !== undefined)
reflectionMergePartial<FriendGameSession>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FriendGameSession): FriendGameSession {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string object_id */ 1:
message.objectId = reader.string();
break;
case /* string shop */ 2:
message.shop = reader.string();
break;
case /* string friend_id */ 3:
message.friendId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: FriendGameSession, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string object_id = 1; */
if (message.objectId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.objectId);
/* string shop = 2; */
if (message.shop !== "")
writer.tag(2, WireType.LengthDelimited).string(message.shop);
/* string friend_id = 3; */
if (message.friendId !== "")
writer.tag(3, WireType.LengthDelimited).string(message.friendId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
return message;
}
internalBinaryWrite(
message: UpdateGamePlaytime,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* int64 playtime_delta_in_seconds = 1; */
if (message.playtimeDeltaInSeconds !== 0n)
writer.tag(1, WireType.Varint).int64(message.playtimeDeltaInSeconds);
/* string last_time_played = 2; */
if (message.lastTimePlayed !== "")
writer.tag(2, WireType.LengthDelimited).string(message.lastTimePlayed);
/* string game_id = 3; */
if (message.gameId !== "")
writer.tag(3, WireType.LengthDelimited).string(message.gameId);
const u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message UpdateGamePlaytime
* @generated MessageType for protobuf message FriendGameSession
*/
export const UpdateGamePlaytime = new UpdateGamePlaytime$Type();
export const FriendGameSession = new FriendGameSession$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Envelope$Type extends MessageType<Envelope> {
constructor() {
super("Envelope", [
{
no: 1,
name: "friend_request",
kind: "message",
oneof: "payload",
T: () => FriendRequest,
},
{
no: 2,
name: "update_game_playtime",
kind: "message",
oneof: "payload",
T: () => UpdateGamePlaytime,
},
]);
}
create(value?: PartialMessage<Envelope>): Envelope {
const message = globalThis.Object.create(this.messagePrototype!);
message.payload = { oneofKind: undefined };
if (value !== undefined)
reflectionMergePartial<Envelope>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: Envelope
): Envelope {
const message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
const [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* FriendRequest friend_request */ 1:
message.payload = {
oneofKind: "friendRequest",
friendRequest: FriendRequest.internalBinaryRead(
reader,
reader.uint32(),
options,
(message.payload as any).friendRequest
),
};
break;
case /* UpdateGamePlaytime update_game_playtime */ 2:
message.payload = {
oneofKind: "updateGamePlaytime",
updateGamePlaytime: UpdateGamePlaytime.internalBinaryRead(
reader,
reader.uint32(),
options,
(message.payload as any).updateGamePlaytime
),
};
break;
default:
const u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
const d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
constructor() {
super("Envelope", [
{ no: 1, name: "friend_request", kind: "message", oneof: "payload", T: () => FriendRequest },
{ no: 2, name: "friend_game_session", kind: "message", oneof: "payload", T: () => FriendGameSession }
]);
}
create(value?: PartialMessage<Envelope>): Envelope {
const message = globalThis.Object.create((this.messagePrototype!));
message.payload = { oneofKind: undefined };
if (value !== undefined)
reflectionMergePartial<Envelope>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Envelope): Envelope {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* FriendRequest friend_request */ 1:
message.payload = {
oneofKind: "friendRequest",
friendRequest: FriendRequest.internalBinaryRead(reader, reader.uint32(), options, (message.payload as any).friendRequest)
};
break;
case /* FriendGameSession friend_game_session */ 2:
message.payload = {
oneofKind: "friendGameSession",
friendGameSession: FriendGameSession.internalBinaryRead(reader, reader.uint32(), options, (message.payload as any).friendGameSession)
};
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Envelope, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* FriendRequest friend_request = 1; */
if (message.payload.oneofKind === "friendRequest")
FriendRequest.internalBinaryWrite(message.payload.friendRequest, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
/* FriendGameSession friend_game_session = 2; */
if (message.payload.oneofKind === "friendGameSession")
FriendGameSession.internalBinaryWrite(message.payload.friendGameSession, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
return message;
}
internalBinaryWrite(
message: Envelope,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* FriendRequest friend_request = 1; */
if (message.payload.oneofKind === "friendRequest")
FriendRequest.internalBinaryWrite(
message.payload.friendRequest,
writer.tag(1, WireType.LengthDelimited).fork(),
options
).join();
/* UpdateGamePlaytime update_game_playtime = 2; */
if (message.payload.oneofKind === "updateGamePlaytime")
UpdateGamePlaytime.internalBinaryWrite(
message.payload.updateGamePlaytime,
writer.tag(2, WireType.LengthDelimited).fork(),
options
).join();
const u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message Envelope

View File

@@ -1,16 +1,21 @@
import { Aria2, DownloadManager, Ludusavi, startMainLoop } from "./services";
import { RealDebridClient } from "./services/download/real-debrid";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
import { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es";
import { Downloader } from "@shared";
import { levelKeys, db } from "./level";
import type { UserPreferences } from "@types";
import { TorBoxClient } from "./services/download/torbox";
import { CommonRedistManager } from "./services/common-redist-manager";
import { WSManager } from "./services/ws-manager";
import { SystemPath } from "./services/system-path";
import {
WSClient,
SystemPath,
CommonRedistManager,
TorBoxClient,
RealDebridClient,
Aria2,
DownloadManager,
Ludusavi,
HydraApi,
uploadGamesBatch,
startMainLoop,
} from "@main/services";
export const loadState = async () => {
SystemPath.checkIfPathsAreAvailable();
@@ -38,7 +43,7 @@ export const loadState = async () => {
await HydraApi.setupApi().then(() => {
uploadGamesBatch();
WSManager.connect();
WSClient.connect();
});
const downloads = await downloadsSublevel

View File

@@ -1 +1,3 @@
export * from "./download-manager";
export * from "./real-debrid";
export * from "./torbox";

View File

@@ -11,7 +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";
import { WSClient } from "./ws/ws-client";
interface HydraApiOptions {
needsAuth?: boolean;
@@ -102,8 +102,8 @@ export class HydraApi {
WindowManager.mainWindow.webContents.send("on-signin");
await clearGamesRemoteIds();
uploadGamesBatch();
WSManager.close();
WSManager.connect();
WSClient.close();
WSClient.connect();
}
}

View File

@@ -12,3 +12,6 @@ export * from "./7zip";
export * from "./game-files-manager";
export * from "./common-redist-manager";
export * from "./aria2";
export * from "./ws";
export * from "./system-path";
export * from "./library-sync";

View File

@@ -10,7 +10,7 @@ import icon from "@resources/icon.png?asset";
import { NotificationOptions, toXmlString } from "./xml";
import { logger } from "../logger";
import { WindowManager } from "../window-manager";
import type { Game, UserPreferences } from "@types";
import type { Game, GameStats, UserPreferences, UserProfile } from "@types";
import { db, levelKeys } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
import { SystemPath } from "../system-path";
@@ -82,7 +82,7 @@ export const publishNotificationUpdateReadyToInstall = async (
};
export const publishNewFriendRequestNotification = async (
senderProfileImageUrl?: string
user: UserProfile
) => {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
@@ -99,9 +99,26 @@ export const publishNewFriendRequestNotification = async (
}),
body: t("new_friend_request_description", {
ns: "notifications",
displayName: user.displayName,
}),
icon: senderProfileImageUrl
? await downloadImage(senderProfileImageUrl)
icon: user?.profileImageUrl
? await downloadImage(user.profileImageUrl)
: trayIcon,
}).show();
};
export const publishFriendStartedPlayingGameNotification = async (
friend: UserProfile,
game: GameStats
) => {
new Notification({
title: t("friend_started_playing_game", {
ns: "notifications",
displayName: friend.displayName,
}),
body: game.assets?.title,
icon: friend?.profileImageUrl
? await downloadImage(friend.profileImageUrl)
: trayIcon,
}).show();
};

View File

@@ -1,75 +0,0 @@
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 | 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");
this.ws = new WebSocket(import.meta.env.MAIN_VITE_WS_URL, {
headers: {
Authorization: `Bearer ${token}`,
},
});
this.ws.on("open", () => {
logger.info("WS connected");
this.reconnectInterval = 1000;
this.reconnecting = false;
});
this.ws.on("message", (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;
}
}
}