feat: Kinematic Instant Messaging #801

Merged
Sainan merged 2 commits from pom2 into main 2025-01-18 16:57:25 -08:00
5 changed files with 219 additions and 2 deletions

View File

@ -0,0 +1,23 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const clearDialogueHistoryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
coderabbitai[bot] commented 2025-01-17 21:00:19 -08:00 (Migrated from github.com)
Review

⚠️ Potential issue

Add error handling for JSON parsing.

The JSON parsing operation could throw an exception if the request body is malformed.

Apply this diff to add error handling:

-    const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
+    let request: IClearDialogueRequest;
+    try {
+        request = JSON.parse(String(req.body)) as IClearDialogueRequest;
+    } catch (error) {
+        res.status(400).json({ error: 'Invalid request body' });
+        return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    let request: IClearDialogueRequest;
    try {
        request = JSON.parse(String(req.body)) as IClearDialogueRequest;
    } catch (error) {
        res.status(400).json({ error: 'Invalid request body' });
        return;
    }
_:warning: Potential issue_ **Add error handling for JSON parsing.** The JSON parsing operation could throw an exception if the request body is malformed. Apply this diff to add error handling: ```diff - const request = JSON.parse(String(req.body)) as IClearDialogueRequest; + let request: IClearDialogueRequest; + try { + request = JSON.parse(String(req.body)) as IClearDialogueRequest; + } catch (error) { + res.status(400).json({ error: 'Invalid request body' }); + return; + } ``` <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. `````suggestion let request: IClearDialogueRequest; try { request = JSON.parse(String(req.body)) as IClearDialogueRequest; } catch (error) { res.status(400).json({ error: 'Invalid request body' }); return; } ````` </details> <!-- suggestion_end --> <!-- This is an auto-generated comment by CodeRabbit -->
if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
for (const dialogueName of request.Dialogues) {
const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
if (index != -1) {
inventory.DialogueHistory.Dialogues.splice(index, 1);
}
}
}
coderabbitai[bot] commented 2025-01-17 21:00:20 -08:00 (Migrated from github.com)
Review

⚠️ Potential issue

Add request validation and error handling.

The function should validate the request structure and handle potential errors during inventory operations.

Apply this diff to add validation and error handling:

+    if (!request.Dialogues || !Array.isArray(request.Dialogues)) {
+        res.status(400).json({ error: 'Invalid request: Dialogues array is required' });
+        return;
+    }
+
     if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
         for (const dialogueName of request.Dialogues) {
+            if (typeof dialogueName !== 'string') {
+                res.status(400).json({ error: 'Invalid request: Dialogue names must be strings' });
+                return;
+            }
+
             const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
             if (index != -1) {
                 inventory.DialogueHistory.Dialogues.splice(index, 1);
             }
         }
     }
-    await inventory.save();
-    res.end();
+    try {
+        await inventory.save();
+        res.status(200).json({
+            success: true,
+            message: 'Dialogue history cleared successfully'
+        });
+    } catch (error) {
+        res.status(500).json({ error: 'Failed to save inventory' });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    if (!request.Dialogues || !Array.isArray(request.Dialogues)) {
        res.status(400).json({ error: 'Invalid request: Dialogues array is required' });
        return;
    }

    if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
        for (const dialogueName of request.Dialogues) {
            if (typeof dialogueName !== 'string') {
                res.status(400).json({ error: 'Invalid request: Dialogue names must be strings' });
                return;
            }

            const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
            if (index != -1) {
                inventory.DialogueHistory.Dialogues.splice(index, 1);
            }
        }
    }
    try {
        await inventory.save();
        res.status(200).json({
            success: true,
            message: 'Dialogue history cleared successfully'
        });
    } catch (error) {
        res.status(500).json({ error: 'Failed to save inventory' });
    }
🧰 Tools
🪛 Biome (1.9.4)

[error] 9-9: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

_:warning: Potential issue_ **Add request validation and error handling.** The function should validate the request structure and handle potential errors during inventory operations. Apply this diff to add validation and error handling: ```diff + if (!request.Dialogues || !Array.isArray(request.Dialogues)) { + res.status(400).json({ error: 'Invalid request: Dialogues array is required' }); + return; + } + if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) { for (const dialogueName of request.Dialogues) { + if (typeof dialogueName !== 'string') { + res.status(400).json({ error: 'Invalid request: Dialogue names must be strings' }); + return; + } + const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName); if (index != -1) { inventory.DialogueHistory.Dialogues.splice(index, 1); } } } - await inventory.save(); - res.end(); + try { + await inventory.save(); + res.status(200).json({ + success: true, + message: 'Dialogue history cleared successfully' + }); + } catch (error) { + res.status(500).json({ error: 'Failed to save inventory' }); + } ``` <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. `````suggestion if (!request.Dialogues || !Array.isArray(request.Dialogues)) { res.status(400).json({ error: 'Invalid request: Dialogues array is required' }); return; } if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) { for (const dialogueName of request.Dialogues) { if (typeof dialogueName !== 'string') { res.status(400).json({ error: 'Invalid request: Dialogue names must be strings' }); return; } const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName); if (index != -1) { inventory.DialogueHistory.Dialogues.splice(index, 1); } } } try { await inventory.save(); res.status(200).json({ success: true, message: 'Dialogue history cleared successfully' }); } catch (error) { res.status(500).json({ error: 'Failed to save inventory' }); } ````` </details> <!-- suggestion_end --> <details> <summary>🧰 Tools</summary> <details> <summary>🪛 Biome (1.9.4)</summary> [error] 9-9: Change to an optional chain. Unsafe fix: Change to an optional chain. (lint/complexity/useOptionalChain) </details> </details> <!-- This is an auto-generated comment by CodeRabbit -->
await inventory.save();
res.end();
};
interface IClearDialogueRequest {
Dialogues: string[];
}

View File

@ -0,0 +1,85 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const saveDialogueController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
coderabbitai[bot] commented 2025-01-17 21:00:20 -08:00 (Migrated from github.com)
Review

⚠️ Potential issue

Add error handling for JSON parsing and type validation.

The JSON parsing operation could throw an exception if the request body is malformed.

Apply this diff to add error handling:

-    const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
+    let request: SaveDialogueRequest;
+    try {
+        request = JSON.parse(String(req.body));
+        if (!('YearIteration' in request || 'DialogueName' in request)) {
+            throw new Error('Invalid request type');
+        }
+    } catch (error) {
+        res.status(400).json({ error: 'Invalid request body' });
+        return;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    let request: SaveDialogueRequest;
    try {
        request = JSON.parse(String(req.body));
        if (!('YearIteration' in request || 'DialogueName' in request)) {
            throw new Error('Invalid request type');
        }
    } catch (error) {
        res.status(400).json({ error: 'Invalid request body' });
        return;
    }
_:warning: Potential issue_ **Add error handling for JSON parsing and type validation.** The JSON parsing operation could throw an exception if the request body is malformed. Apply this diff to add error handling: ```diff - const request = JSON.parse(String(req.body)) as SaveDialogueRequest; + let request: SaveDialogueRequest; + try { + request = JSON.parse(String(req.body)); + if (!('YearIteration' in request || 'DialogueName' in request)) { + throw new Error('Invalid request type'); + } + } catch (error) { + res.status(400).json({ error: 'Invalid request body' }); + return; + } ``` <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. `````suggestion let request: SaveDialogueRequest; try { request = JSON.parse(String(req.body)); if (!('YearIteration' in request || 'DialogueName' in request)) { throw new Error('Invalid request type'); } } catch (error) { res.status(400).json({ error: 'Invalid request body' }); return; } ````` </details> <!-- suggestion_end --> <!-- This is an auto-generated comment by CodeRabbit -->
if ("YearIteration" in request) {
const inventory = await getInventory(accountId);
if (inventory.DialogueHistory) {
inventory.DialogueHistory.YearIteration = request.YearIteration;
} else {
inventory.DialogueHistory = { YearIteration: request.YearIteration };
}
await inventory.save();
res.end();
} else {
const inventory = await getInventory(accountId);
if (!inventory.DialogueHistory) {
throw new Error("bad inventory state");
}
if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) {
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
}
inventory.DialogueHistory.Dialogues ??= [];
let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory.Dialogues[
inventory.DialogueHistory.Dialogues.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: request.DialogueName
}) - 1
];
}
dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry;
//dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
}
for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool);
if (index != -1) {
dialogue.Booleans.splice(index, 1);
}
}
dialogue.Completed.push(request.Data);
const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000;
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: [],
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
}
};
type SaveDialogueRequest = SaveYearIterationRequest | SaveCompletedDialogueRequest;
interface SaveYearIterationRequest {
YearIteration: number;
}
interface SaveCompletedDialogueRequest {
DialogueName: string;
Rank: number;
Chemistry: number;
CompletionType: number;
QueuedDialogues: string[]; // unsure
Booleans: string[];
ResetBooleans: string[];
Data: ICompletedDialogue;
OtherDialogueInfos: string[]; // unsure
}

View File

@ -47,7 +47,12 @@ import {
ICrewShipPilotWeapon,
IShipExterior,
IHelminthFoodRecord,
ICrewShipMembersDatabase
ICrewShipMembersDatabase,
IDialogueHistoryDatabase,
IDialogueDatabase,
IDialogueGift,
ICompletedDialogue,
IDialogueClient
} from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes";
import {
@ -710,6 +715,60 @@ crewShipSchema.set("toJSON", {
}
});
const dialogueGiftSchema = new Schema<IDialogueGift>(
{
Item: String,
GiftedQuantity: Number
},
{ _id: false }
);
const completedDialogueSchema = new Schema<ICompletedDialogue>(
{
Id: { type: String, required: true },
Booleans: { type: [String], required: true },
Choices: { type: [Number], required: true }
},
{ _id: false }
);
const dialogueSchema = new Schema<IDialogueDatabase>(
{
Rank: Number,
Chemistry: Number,
AvailableDate: Date,
AvailableGiftDate: Date,
RankUpExpiry: Date,
BountyChemExpiry: Date,
//QueuedDialogues: ???
Gifts: { type: [dialogueGiftSchema], default: [] },
Booleans: { type: [String], default: [] },
Completed: { type: [completedDialogueSchema], default: [] },
DialogueName: String
},
{ _id: false }
);
dialogueSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret) {
const db = ret as IDialogueDatabase;
const client = ret as IDialogueClient;
client.AvailableDate = toMongoDate(db.AvailableDate);
client.AvailableGiftDate = toMongoDate(db.AvailableGiftDate);
client.RankUpExpiry = toMongoDate(db.RankUpExpiry);
client.BountyChemExpiry = toMongoDate(db.BountyChemExpiry);
}
});
const dialogueHistorySchema = new Schema<IDialogueHistoryDatabase>(
{
YearIteration: { type: Number, required: true },
Dialogues: { type: [dialogueSchema], required: false }
},
{ _id: false }
);
const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
{
accountOwnerId: Schema.Types.ObjectId,
@ -1069,7 +1128,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Grustag three
DeathSquadable: Boolean,
EndlessXP: { type: [endlessXpProgressSchema], default: undefined }
EndlessXP: { type: [endlessXpProgressSchema], default: undefined },
DialogueHistory: dialogueHistorySchema
},
{ timestamps: { createdAt: "Created" } }
);

View File

@ -6,6 +6,7 @@ import { archonFusionController } from "@/src/controllers/api/archonFusionContro
import { artifactsController } from "../controllers/api/artifactsController";
import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController";
import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompletedRecipeController";
import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController";
import { createGuildController } from "@/src/controllers/api/createGuildController";
import { creditsController } from "@/src/controllers/api/creditsController";
import { deleteSessionController } from "@/src/controllers/api/deleteSessionController";
@ -52,6 +53,7 @@ import { projectionManagerController } from "../controllers/api/projectionManage
import { purchaseController } from "@/src/controllers/api/purchaseController";
import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController";
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
import { saveLoadoutController } from "@/src/controllers/api/saveLoadout";
import { sellController } from "@/src/controllers/api/sellController";
import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController";
@ -118,6 +120,7 @@ apiRouter.post("/arcaneCommon.php", arcaneCommonController);
apiRouter.post("/archonFusion.php", archonFusionController);
apiRouter.post("/artifacts.php", artifactsController);
apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController);
apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController);
apiRouter.post("/createGuild.php", createGuildController);
apiRouter.post("/endlessXp.php", endlessXpController);
apiRouter.post("/evolveWeapon.php", evolveWeaponController);
@ -142,6 +145,7 @@ apiRouter.post("/playerSkills.php", playerSkillsController);
apiRouter.post("/projectionManager.php", projectionManagerController);
apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
apiRouter.post("/saveDialogue.php", saveDialogueController);
apiRouter.post("/saveLoadout.php", saveLoadoutController);
apiRouter.post("/sell.php", sellController);
apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController);

View File

@ -306,6 +306,7 @@ export interface IInventoryResponse extends IDailyAffiliations {
Harvestable: boolean;
DeathSquadable: boolean;
EndlessXP?: IEndlessXpProgress[];
DialogueHistory?: IDialogueHistoryDatabase;
}
export interface IAffiliation {
@ -948,3 +949,46 @@ export interface IEndlessXpProgress {
Category: TEndlessXpCategory;
Choices: string[];
}
export interface IDialogueHistoryClient {
YearIteration: number;
Dialogues?: IDialogueClient[];
}
export interface IDialogueHistoryDatabase {
YearIteration: number;
Dialogues?: IDialogueDatabase[];
}
export interface IDialogueClient {
Rank: number;
Chemistry: number;
AvailableDate: IMongoDate;
AvailableGiftDate: IMongoDate;
RankUpExpiry: IMongoDate;
BountyChemExpiry: IMongoDate;
//QueuedDialogues: any[];
Gifts: IDialogueGift[];
Booleans: string[];
Completed: ICompletedDialogue[];
DialogueName: string;
}
export interface IDialogueDatabase
extends Omit<IDialogueClient, "AvailableDate" | "AvailableGiftDate" | "RankUpExpiry" | "BountyChemExpiry"> {
AvailableDate: Date;
AvailableGiftDate: Date;
RankUpExpiry: Date;
BountyChemExpiry: Date;
}
export interface IDialogueGift {
Item: string;
GiftedQuantity: number;
}
export interface ICompletedDialogue {
Id: string;
Booleans: string[];
Choices: number[];
}