From 50c280cf01013e6bdf48256647ff495aca3c9986 Mon Sep 17 00:00:00 2001 From: OrdisPrime <134585663+OrdisPrime@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:15:36 +0100 Subject: [PATCH] feat: Inbox (#876) --- .eslintrc | 4 +- src/controllers/api/inboxController.ts | 85 +++++++++++- src/controllers/api/loginController.ts | 5 +- .../custom/createMessageController.ts | 14 ++ src/helpers/customHelpers/customHelpers.ts | 3 +- src/models/inboxModel.ts | 130 ++++++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 19 ++- src/models/loginModel.ts | 18 +-- src/routes/custom.ts | 2 + src/services/inboxService.ts | 66 +++++++++ src/services/inventoryService.ts | 12 ++ src/types/inventoryTypes/inventoryTypes.ts | 16 ++- src/types/loginTypes.ts | 45 +++--- .../{inbox.json => messages.json} | 20 ++- 14 files changed, 368 insertions(+), 71 deletions(-) create mode 100644 src/controllers/custom/createMessageController.ts create mode 100644 src/models/inboxModel.ts create mode 100644 src/services/inboxService.ts rename static/fixed_responses/{inbox.json => messages.json} (72%) diff --git a/.eslintrc b/.eslintrc index 90844d33..f03e5269 100644 --- a/.eslintrc +++ b/.eslintrc @@ -26,7 +26,9 @@ "no-case-declarations": "warn", "prettier/prettier": "error", "@typescript-eslint/semi": "error", - "no-mixed-spaces-and-tabs": "error" + "no-mixed-spaces-and-tabs": "error", + "require-await": "off", + "@typescript-eslint/require-await": "error" }, "parser": "@typescript-eslint/parser", "parserOptions": { diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index cbd2af06..d604f253 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -1,8 +1,83 @@ import { RequestHandler } from "express"; -import inbox from "@/static/fixed_responses/inbox.json"; +import { Inbox } from "@/src/models/inboxModel"; +import { + createNewEventMessages, + deleteAllMessagesRead, + deleteMessageRead, + getAllMessagesSorted, + getMessage +} from "@/src/services/inboxService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { addItems, getInventory } from "@/src/services/inventoryService"; +import { logger } from "@/src/utils/logger"; -const inboxController: RequestHandler = (_req, res) => { - res.json(inbox); +export const inboxController: RequestHandler = async (req, res) => { + const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; + + const accountId = await getAccountIdForRequest(req); + + if (deleteId) { + if (deleteId === "DeleteAllRead") { + await deleteAllMessagesRead(accountId); + res.status(200).end(); + return; + } + + await deleteMessageRead(deleteId as string); + res.status(200).end(); + } else if (messageId) { + const message = await getMessage(messageId as string); + message.r = true; + const attachmentItems = message.att; + const attachmentCountedItems = message.countedAtt; + + if (!attachmentItems && !attachmentCountedItems) { + await message.save(); + + res.status(200).end(); + return; + } + + const inventory = await getInventory(accountId); + const inventoryChanges = {}; + if (attachmentItems) { + await addItems( + inventory, + attachmentItems.map(attItem => ({ ItemType: attItem, ItemCount: 1 })), + inventoryChanges + ); + } + if (attachmentCountedItems) { + await addItems(inventory, attachmentCountedItems, inventoryChanges); + } + await inventory.save(); + await message.save(); + + res.json({ InventoryChanges: inventoryChanges }); + } else if (latestClientMessageId) { + await createNewEventMessages(req); + const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 }); + + const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId); + + if (!latestClientMessage) { + logger.debug(`this should only happen after DeleteAllRead `); + res.json({ Inbox: messages }); + return; + } + const newMessages = messages.filter(m => m.date > latestClientMessage.date); + + if (newMessages.length === 0) { + res.send("no-new"); + return; + } + + res.json({ Inbox: newMessages }); + } else { + //newly created event messages must be newer than account.LatestEventMessageDate + await createNewEventMessages(req); + const messages = await getAllMessagesSorted(accountId); + const inbox = messages.map(m => m.toJSON()); + res.json({ Inbox: inbox }); + } }; - -export { inboxController }; diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 452cd61b..00f1380c 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -12,7 +12,7 @@ import { logger } from "@/src/utils/logger"; export const loginController: RequestHandler = async (request, response) => { const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object - const account = await Account.findOne({ email: loginRequest.email }); //{ _id: 0, __v: 0 } + const account = await Account.findOne({ email: loginRequest.email }); const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER); const buildLabel: string = @@ -41,7 +41,8 @@ export const loginController: RequestHandler = async (request, response) => { ForceLogoutVersion: 0, ConsentNeeded: false, TrackedSettings: [], - Nonce: nonce + Nonce: nonce, + LatestEventMessageDate: new Date(0) }); logger.debug("created new account"); response.json(createLoginResponse(newAccount, buildLabel)); diff --git a/src/controllers/custom/createMessageController.ts b/src/controllers/custom/createMessageController.ts new file mode 100644 index 00000000..af67fd2d --- /dev/null +++ b/src/controllers/custom/createMessageController.ts @@ -0,0 +1,14 @@ +import { createMessage, IMessageCreationTemplate } from "@/src/services/inboxService"; +import { RequestHandler } from "express"; + +export const createMessageController: RequestHandler = async (req, res) => { + const message = req.body as (IMessageCreationTemplate & { ownerId: string })[] | undefined; + + if (!message) { + res.status(400).send("No message provided"); + return; + } + const savedMessages = await createMessage(message[0].ownerId, message); + + res.json(savedMessages); +}; diff --git a/src/helpers/customHelpers/customHelpers.ts b/src/helpers/customHelpers/customHelpers.ts index cb4d154a..e1173d1f 100644 --- a/src/helpers/customHelpers/customHelpers.ts +++ b/src/helpers/customHelpers/customHelpers.ts @@ -48,7 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount => CrossPlatformAllowed: true, ForceLogoutVersion: 0, TrackedSettings: [], - Nonce: 0 + Nonce: 0, + LatestEventMessageDate: new Date(0) } satisfies IDatabaseAccount; }; diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts new file mode 100644 index 00000000..9469c3e5 --- /dev/null +++ b/src/models/inboxModel.ts @@ -0,0 +1,130 @@ +import { model, Schema, Types } from "mongoose"; +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { typeCountSchema } from "@/src/models/inventoryModels/inventoryModel"; +import { IMongoDate, IOid } from "@/src/types/commonTypes"; +import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; + +export interface IMessageClient extends Omit { + _id?: IOid; + date: IMongoDate; + startDate?: IMongoDate; + endDate?: IMongoDate; + messageId: IOid; +} + +export interface IMessageDatabase { + ownerId: Types.ObjectId; + date: Date; + _id: Types.ObjectId; + sndr: string; + msg: string; + sub: string; + icon: string; + highPriority?: boolean; + lowPrioNewPlayers?: boolean; + startDate?: Date; + endDate?: Date; + r?: boolean; + att?: string[]; + countedAtt?: ITypeCount[]; + transmission?: string; + arg?: Arg[]; +} + +export interface Arg { + Key: string; + Tag: string; +} + +//types are wrong +// export interface IMessageDatabase { +// _id: Types.ObjectId; +// messageId: string; +// sub: string; +// sndr: string; +// msg: string; +// startDate: Date; +// endDate: Date; +// date: Date; +// contextInfo: string; +// icon: string; +// att: string[]; +// modPacks: string[]; +// countedAtt: string[]; +// attSpecial: string[]; +// transmission: string; +// ordisReactionTransmission: string; +// arg: string[]; +// r: string; +// acceptAction: string; +// declineAction: string; +// highPriority: boolean; +// lowPrioNewPlayers: boolean +// gifts: string[]; +// teleportLoc: string; +// RegularCredits: string; +// PremiumCredits: string; +// PrimeTokens: string; +// Coupons: string[]; +// syndicateAttachment: string[]; +// tutorialTag: string; +// url: string; +// urlButtonText: string; +// cinematic: string; +// requiredLevel: string; +// } +const messageSchema = new Schema( + { + ownerId: Schema.Types.ObjectId, + sndr: String, + msg: String, + sub: String, + icon: String, + highPriority: Boolean, + lowPrioNewPlayers: Boolean, + startDate: Date, + endDate: Date, + r: Boolean, + att: { type: [String], default: undefined }, + countedAtt: { type: [typeCountSchema], default: undefined }, + transmission: String, + arg: { + type: [ + { + Key: String, + Tag: String, + _id: false + } + ], + default: undefined + } + }, + { timestamps: { createdAt: "date", updatedAt: false }, id: false } +); + +messageSchema.virtual("messageId").get(function (this: IMessageDatabase) { + return toOid(this._id); +}); + +messageSchema.set("toJSON", { + virtuals: true, + transform(_document, returnedObject) { + delete returnedObject.ownerId; + + const messageDatabase = returnedObject as IMessageDatabase; + const messageClient = returnedObject as IMessageClient; + + delete returnedObject._id; + delete returnedObject.__v; + + messageClient.date = toMongoDate(messageDatabase.date); + + if (messageDatabase.startDate && messageDatabase.endDate) { + messageClient.startDate = toMongoDate(messageDatabase.startDate); + + messageClient.endDate = toMongoDate(messageDatabase.endDate); + } + } +}); + +export const Inbox = model("Inbox", messageSchema, "inbox"); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 2bb888d2..a8006c38 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1,4 +1,4 @@ -import { Document, Model, Schema, Types, model } from "mongoose"; +import { Document, HydratedDocument, Model, Schema, Types, model } from "mongoose"; import { IFlavourItem, IRawUpgrade, @@ -7,7 +7,7 @@ import { IBooster, IInventoryClient, ISlots, - IMailbox, + IMailboxDatabase, IDuviriInfo, IPendingRecipe as IPendingRecipeDatabase, IPendingRecipeResponse, @@ -53,6 +53,7 @@ import { IUpgradeDatabase, ICrewShipMemberDatabase, ICrewShipMemberClient, + IMailboxClient, TEquipmentKey, equipmentKeys, IKubrowPetDetailsDatabase, @@ -298,22 +299,18 @@ FlavourItemSchema.set("toJSON", { } }); -// "Mailbox": { "LastInboxId": { "$oid": "123456780000000000000000" } } -const MailboxSchema = new Schema( +const MailboxSchema = new Schema( { - LastInboxId: { - type: Schema.Types.ObjectId, - set: (v: IMailbox["LastInboxId"]): string => v.$oid.toString() - } + LastInboxId: Schema.Types.ObjectId }, { id: false, _id: false } ); MailboxSchema.set("toJSON", { transform(_document, returnedObject) { - delete returnedObject.__v; - //TODO: there is a lot of any here - returnedObject.LastInboxId = toOid(returnedObject.LastInboxId as Types.ObjectId); + const mailboxDatabase = returnedObject as HydratedDocument; + delete mailboxDatabase.__v; + (returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId); } }); diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 258497ad..e52b8d0b 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -6,20 +6,6 @@ const opts = { toObject: { virtuals: true } } satisfies SchemaOptions; -// { -// toJSON: { virtuals: true } -// } -// { -// virtuals: { -// id: { -// get() { -// return "test"; -// } -// }, -// toJSON: { virtuals: true } -// } -// } - const databaseAccountSchema = new Schema( { email: { type: String, required: true, unique: true }, @@ -34,14 +20,14 @@ const databaseAccountSchema = new Schema( ConsentNeeded: { type: Boolean, required: true }, TrackedSettings: { type: [String], default: [] }, Nonce: { type: Number, default: 0 }, - LastLoginDay: { type: Number } + LastLoginDay: { type: Number }, + LatestEventMessageDate: { type: Date, required: true } }, opts ); databaseAccountSchema.set("toJSON", { transform(_document, returnedObject) { - //returnedObject.id = returnedObject._id.toString(); delete returnedObject._id; delete returnedObject.__v; }, diff --git a/src/routes/custom.ts b/src/routes/custom.ts index ad11383e..3ada9830 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -14,6 +14,7 @@ import { importController } from "@/src/controllers/custom/importController"; import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController"; +import { createMessageController } from "@/src/controllers/custom/createMessageController"; const customRouter = express.Router(); @@ -25,6 +26,7 @@ customRouter.get("/deleteAccount", deleteAccountController); customRouter.get("/renameAccount", renameAccountController); customRouter.post("/createAccount", createAccountController); +customRouter.post("/createMessage", createMessageController); customRouter.post("/addItems", addItemsController); customRouter.post("/addXp", addXpController); customRouter.post("/import", importController); diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts new file mode 100644 index 00000000..81cc443e --- /dev/null +++ b/src/services/inboxService.ts @@ -0,0 +1,66 @@ +import { IMessageDatabase, Inbox } from "@/src/models/inboxModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { HydratedDocument } from "mongoose"; +import { Request } from "express"; +import messages from "@/static/fixed_responses/messages.json"; +import { logger } from "@/src/utils/logger"; + +export const getAllMessagesSorted = async (accountId: string): Promise[]> => { + const inbox = await Inbox.find({ ownerId: accountId }).sort({ date: -1 }); + return inbox; +}; + +export const getMessage = async (messageId: string): Promise> => { + const message = await Inbox.findOne({ _id: messageId }); + + if (!message) { + throw new Error(`Message not found ${messageId}`); + } + return message; +}; + +export const deleteMessageRead = async (messageId: string): Promise => { + await Inbox.findOneAndDelete({ _id: messageId, r: true }); +}; + +export const deleteAllMessagesRead = async (accountId: string): Promise => { + await Inbox.deleteMany({ ownerId: accountId, r: true }); +}; + +export const createNewEventMessages = async (req: Request) => { + const account = await getAccountForRequest(req); + const latestEventMessageDate = account.LatestEventMessageDate; + + //TODO: is baroo there? create these kind of messages too (periodical messages) + const newEventMessages = messages.Messages.filter(m => new Date(m.eventMessageDate) > latestEventMessageDate); + + if (newEventMessages.length === 0) { + logger.debug(`No new event messages. Latest event message date: ${latestEventMessageDate.toISOString()}`); + return; + } + + const savedEventMessages = await createMessage(account._id.toString(), newEventMessages); + logger.debug("created event messages", savedEventMessages); + + const latestEventMessage = newEventMessages.reduce((prev, current) => + prev.eventMessageDate > current.eventMessageDate ? prev : current + ); + + console.log("latestEventMessage", latestEventMessage); + account.LatestEventMessageDate = new Date(latestEventMessage.eventMessageDate); + await account.save(); +}; + +export const createMessage = async (accountId: string, messages: IMessageCreationTemplate[]) => { + const ownerIdMessages = messages.map(m => ({ + ...m, + ownerId: accountId + })); + + const savedMessages = await Inbox.insertMany(ownerIdMessages); + return savedMessages; +}; + +export interface IMessageCreationTemplate extends Omit { + ownerId?: string; +} diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ddd5acc4..97f1c773 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -474,6 +474,18 @@ export const addItem = async ( throw new Error(errorMessage); }; +export const addItems = async ( + inventory: TInventoryDatabaseDocument, + items: ITypeCount[], + inventoryChanges: IInventoryChanges = {} +): Promise => { + for (const item of items) { + const inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount); + combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges); + } + return inventoryChanges; +}; + //TODO: maybe genericMethod for all the add methods, they share a lot of logic export const addSentinel = ( inventory: TInventoryDatabaseDocument, diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index b80134be..0fca78b8 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -37,10 +37,10 @@ export interface IInventoryDatabase > { accountOwnerId: Types.ObjectId; Created: Date; - TrainingDate: Date; // TrainingDate changed from IMongoDate to Date + TrainingDate: Date; LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population - Mailbox: Types.ObjectId; // Mailbox changed from IMailbox to Types.ObjectId - GuildId?: Types.ObjectId; // GuildId changed from ?IOid to ?Types.ObjectId + Mailbox?: IMailboxDatabase; + GuildId?: Types.ObjectId; PendingRecipes: IPendingRecipe[]; QuestKeys: IQuestKeyDatabase[]; BlessingCooldown: Date; @@ -127,10 +127,14 @@ export interface IDuviriInfo { NumCompletions: number; } -export interface IMailbox { +export interface IMailboxClient { LastInboxId: IOid; } +export interface IMailboxDatabase { + LastInboxId: Types.ObjectId; +} + export type TSolarMapRegion = | "Earth" | "Ceres" @@ -202,7 +206,7 @@ export interface IInventoryClient extends IDailyAffiliations { KahlLoadOuts: IOperatorConfigClient[]; DuviriInfo: IDuviriInfo; - Mailbox: IMailbox; + Mailbox?: IMailboxClient; SubscribedToEmails: number; Created: IMongoDate; RewardSeed: number; @@ -238,7 +242,7 @@ export interface IInventoryClient extends IDailyAffiliations { ActiveQuest: string; FlavourItems: IFlavourItem[]; LoadOutPresets: ILoadOutPresets; - CurrentLoadOutIds: IOid[]; // we store it in the database using this representation as well :/ + CurrentLoadOutIds: IOid[]; //TODO: we store it in the database using this representation as well :/ Missions: IMission[]; RandomUpgradesIdentified?: number; LastRegionPlayed: TSolarMapRegion; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 2a229b0e..728fde52 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -11,6 +11,29 @@ export interface IAccountAndLoginResponseCommons { Nonce: number; } +export interface IDatabaseAccount extends IAccountAndLoginResponseCommons { + email: string; + password: string; + LastLoginDay?: number; + LatestEventMessageDate: Date; +} + +// Includes virtual ID +export interface IDatabaseAccountJson extends IDatabaseAccount { + id: string; +} + +export interface ILoginRequest { + email: string; + password: string; + time: number; + s: string; + lang: string; + date: number; + ClientType: string; + PS: string; +} + export interface ILoginResponse extends IAccountAndLoginResponseCommons { id: string; Groups: IGroup[]; @@ -23,29 +46,7 @@ export interface ILoginResponse extends IAccountAndLoginResponseCommons { HUB: string; } -// Includes virtual ID -export interface IDatabaseAccountJson extends IDatabaseAccount { - id: string; -} - export interface IGroup { experiment: string; experimentGroup: string; } - -export interface IDatabaseAccount extends IAccountAndLoginResponseCommons { - email: string; - password: string; - LastLoginDay?: number; -} - -export interface ILoginRequest { - email: string; - password: string; - time: number; - s: string; - lang: string; - date: number; - ClientType: string; - PS: string; -} diff --git a/static/fixed_responses/inbox.json b/static/fixed_responses/messages.json similarity index 72% rename from static/fixed_responses/inbox.json rename to static/fixed_responses/messages.json index f6334b54..a67069c7 100644 --- a/static/fixed_responses/inbox.json +++ b/static/fixed_responses/messages.json @@ -1,5 +1,13 @@ { - "Inbox": [ + "Messages": [ + { + "sub": "Welcome to Space Ninja Server", + "sndr": "/Lotus/Language/Bosses/Ordis", + "msg": "Enjoy your Space Ninja Experience", + "icon": "/Lotus/Interface/Icons/Npcs/Darvo.png", + "eventMessageDate": "2025-01-30T13:00:00.000Z", + "r": false + }, { "sub": "/Lotus/Language/Inbox/DarvoWeaponCraftingMessageBTitle", "sndr": "/Lotus/Language/Bosses/Darvo", @@ -24,9 +32,8 @@ } ], "highPriority": true, - "messageId": "66d651800000000000000000", - "date": { "$date": { "$numberLong": "1725321600000" } }, - "r": true + "eventMessageDate": "2023-10-01T17:00:00.000Z", + "r": false }, { "sub": "/Lotus/Language/G1Quests/Beginner_Growth_Inbox_Title", @@ -35,9 +42,8 @@ "icon": "/Lotus/Interface/Icons/Npcs/Lotus_d.png", "transmission": "/Lotus/Sounds/Dialog/VorsPrize/DLisetPostAssassinate110Lotus", "highPriority": true, - "messageId": "66d651810000000000000000", - "date": { "$date": { "$numberLong": "1725321601000" } }, - "r": true + "eventMessageDate": "2023-09-01T17:00:00.000Z", + "r": false } ] }