From 36d2b2dda543d283338472ea9dd56093980c00e2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:57:44 -0700 Subject: [PATCH] feat: gifting (#1344) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1344 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/giftingController.ts | 92 ++++++++++++++++++++++ src/controllers/api/inboxController.ts | 55 ++++++++++--- src/controllers/api/inventoryController.ts | 1 + src/models/inboxModel.ts | 14 ++++ src/routes/api.ts | 2 + src/services/loginService.ts | 4 + 6 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 src/controllers/api/giftingController.ts diff --git a/src/controllers/api/giftingController.ts b/src/controllers/api/giftingController.ts new file mode 100644 index 00000000..ce3e5a30 --- /dev/null +++ b/src/controllers/api/giftingController.ts @@ -0,0 +1,92 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account } from "@/src/models/loginModel"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { IOid } from "@/src/types/commonTypes"; +import { IPurchaseParams } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const giftingController: RequestHandler = async (req, res) => { + const data = getJSONfromString(String(req.body)); + if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) { + throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`); + } + + const account = await Account.findOne( + data.RecipientId ? { _id: data.RecipientId.$oid } : { DisplayName: data.Recipient } + ); + if (!account) { + res.status(400).send("9").end(); + return; + } + const inventory = await getInventory(account._id.toString(), "Suits Settings"); + + // Cannot gift items to players that have not completed the tutorial. + if (inventory.Suits.length == 0) { + res.status(400).send("14").end(); + return; + } + + // Cannot gift to players who have gifting disabled. + // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented + if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") { + res.status(400).send("17").end(); + return; + } + + // TODO: Cannot gift items with mastery requirement to players who are too low level. (Code 2) + // TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7) + // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20) + + const senderAccount = await getAccountForRequest(req); + const senderInventory = await getInventory( + senderAccount._id.toString(), + "PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining" + ); + + if (senderInventory.GiftsRemaining == 0) { + res.status(400).send("10").end(); + return; + } + senderInventory.GiftsRemaining -= 1; + + updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true); + await senderInventory.save(); + + const senderName = getSuffixedName(senderAccount); + await createMessage(account._id.toString(), [ + { + sndr: senderName, + msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage", + arg: [ + { + Key: "GIFTER_NAME", + Tag: senderName + }, + { + Key: "GIFT_QUANTITY", + Tag: data.PurchaseParams.Quantity + } + ], + sub: "/Lotus/Language/Menu/GiftReceivedSubject", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + gifts: [ + { + GiftType: data.PurchaseParams.StoreItem + } + ] + } + ]); + + res.end(); +}; + +interface IGiftingRequest { + PurchaseParams: IPurchaseParams; + Message?: string; + Recipient?: string; + RecipientId?: IOid; + buildLabel: string; +} diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 896c01b6..84ebf03e 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -1,21 +1,24 @@ import { RequestHandler } from "express"; import { Inbox } from "@/src/models/inboxModel"; import { + createMessage, createNewEventMessages, deleteAllMessagesRead, deleteMessageRead, getAllMessagesSorted, getMessage } from "@/src/services/inboxService"; -import { getAccountIdForRequest } from "@/src/services/loginService"; -import { addItems, getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; +import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { logger } from "@/src/utils/logger"; -import { ExportGear } from "warframe-public-export-plus"; +import { ExportFlavour, ExportGear } from "warframe-public-export-plus"; +import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; export const inboxController: RequestHandler = async (req, res) => { const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); + const accountId = account._id.toString(); if (deleteId) { if (deleteId === "DeleteAllRead") { @@ -29,12 +32,12 @@ export const inboxController: RequestHandler = async (req, res) => { } else if (messageId) { const message = await getMessage(messageId as string); message.r = true; + await message.save(); + const attachmentItems = message.att; const attachmentCountedItems = message.countedAtt; - if (!attachmentItems && !attachmentCountedItems) { - await message.save(); - + if (!attachmentItems && !attachmentCountedItems && !message.gifts) { res.status(200).end(); return; } @@ -54,9 +57,43 @@ export const inboxController: RequestHandler = async (req, res) => { if (attachmentCountedItems) { await addItems(inventory, attachmentCountedItems, inventoryChanges); } + if (message.gifts) { + const sender = await getAccountFromSuffixedName(message.sndr); + const recipientName = getSuffixedName(account); + const giftQuantity = message.arg!.find(x => x.Key == "GIFT_QUANTITY")!.Tag as number; + for (const gift of message.gifts) { + combineInventoryChanges( + inventoryChanges, + (await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges + ); + if (sender) { + await createMessage(sender._id.toString(), [ + { + sndr: recipientName, + msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody", + arg: [ + { + Key: "RECIPIENT_NAME", + Tag: recipientName + }, + { + Key: "GIFT_TYPE", + Tag: gift.GiftType + }, + { + Key: "GIFT_QUANTITY", + Tag: giftQuantity + } + ], + sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject", + icon: ExportFlavour[inventory.ActiveAvatarImageType].icon, + highPriority: true + } + ]); + } + } + } await inventory.save(); - await message.save(); - res.json({ InventoryChanges: inventoryChanges }); } else if (latestClientMessageId) { await createNewEventMessages(req); diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 36b1221d..e2f4a99d 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -33,6 +33,7 @@ export const inventoryController: RequestHandler = async (request, response) => inventory[key] = 16000 + inventory.PlayerLevel * 500; } inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; + inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); diff --git a/src/models/inboxModel.ts b/src/models/inboxModel.ts index c3ad8add..c2d8af44 100644 --- a/src/models/inboxModel.ts +++ b/src/models/inboxModel.ts @@ -31,6 +31,7 @@ export interface IMessage { countedAtt?: ITypeCount[]; transmission?: string; arg?: Arg[]; + gifts?: IGift[]; r?: boolean; contextInfo?: string; acceptAction?: string; @@ -43,6 +44,10 @@ export interface Arg { Tag: string | number; } +export interface IGift { + GiftType: string; +} + //types are wrong // export interface IMessageDatabase { // _id: Types.ObjectId; @@ -80,6 +85,14 @@ export interface Arg { // cinematic: string; // requiredLevel: string; // } + +const giftSchema = new Schema( + { + GiftType: String + }, + { _id: false } +); + const messageSchema = new Schema( { ownerId: Schema.Types.ObjectId, @@ -93,6 +106,7 @@ const messageSchema = new Schema( endDate: Date, r: Boolean, att: { type: [String], default: undefined }, + gifts: { type: [giftSchema], default: undefined }, countedAtt: { type: [typeCountSchema], default: undefined }, transmission: String, arg: { diff --git a/src/routes/api.ts b/src/routes/api.ts index 30d93d0e..8fd82e30 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -51,6 +51,7 @@ import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSe import { getShipController } from "@/src/controllers/api/getShipController"; import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController"; import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController"; +import { giftingController } from "@/src/controllers/api/giftingController"; import { gildWeaponController } from "@/src/controllers/api/gildWeaponController"; import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; @@ -203,6 +204,7 @@ apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getFriends.php", getFriendsController); apiRouter.post("/getGuildDojo.php", getGuildDojoController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); +apiRouter.post("/gifting.php", giftingController); apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 77ffc95c..df9e6c90 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -100,3 +100,7 @@ export const getSuffixedName = (account: TAccountDocument): string => { const suffix = ((crc32.str(name.toLowerCase() + "595") >>> 0) + platform_magics[platformId]) % 1000; return name + "#" + suffix.toString().padStart(3, "0"); }; + +export const getAccountFromSuffixedName = (name: string): Promise => { + return Account.findOne({ DisplayName: name.split("#")[0] }); +};