feat: gifting (#1344)
Some checks are pending
Build / build (18) (push) Waiting to run
Build / build (20) (push) Waiting to run
Build / build (22) (push) Waiting to run
Build Docker image / docker (push) Waiting to run

Reviewed-on: #1344
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-03-27 12:57:44 -07:00 committed by Sainan
parent a56ff89bb9
commit 36d2b2dda5
6 changed files with 159 additions and 9 deletions

View File

@ -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<IGiftingRequest>(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;
}

View File

@ -1,21 +1,24 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Inbox } from "@/src/models/inboxModel"; import { Inbox } from "@/src/models/inboxModel";
import { import {
createMessage,
createNewEventMessages, createNewEventMessages,
deleteAllMessagesRead, deleteAllMessagesRead,
deleteMessageRead, deleteMessageRead,
getAllMessagesSorted, getAllMessagesSorted,
getMessage getMessage
} from "@/src/services/inboxService"; } from "@/src/services/inboxService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
import { addItems, getInventory } from "@/src/services/inventoryService"; import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; 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) => { export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; 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) {
if (deleteId === "DeleteAllRead") { if (deleteId === "DeleteAllRead") {
@ -29,12 +32,12 @@ export const inboxController: RequestHandler = async (req, res) => {
} else if (messageId) { } else if (messageId) {
const message = await getMessage(messageId as string); const message = await getMessage(messageId as string);
message.r = true; message.r = true;
await message.save();
const attachmentItems = message.att; const attachmentItems = message.att;
const attachmentCountedItems = message.countedAtt; const attachmentCountedItems = message.countedAtt;
if (!attachmentItems && !attachmentCountedItems) { if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
await message.save();
res.status(200).end(); res.status(200).end();
return; return;
} }
@ -54,9 +57,43 @@ export const inboxController: RequestHandler = async (req, res) => {
if (attachmentCountedItems) { if (attachmentCountedItems) {
await addItems(inventory, attachmentCountedItems, inventoryChanges); 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 inventory.save();
await message.save();
res.json({ InventoryChanges: inventoryChanges }); res.json({ InventoryChanges: inventoryChanges });
} else if (latestClientMessageId) { } else if (latestClientMessageId) {
await createNewEventMessages(req); await createNewEventMessages(req);

View File

@ -33,6 +33,7 @@ export const inventoryController: RequestHandler = async (request, response) =>
inventory[key] = 16000 + inventory.PlayerLevel * 500; inventory[key] = 16000 + inventory.PlayerLevel * 500;
} }
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();

View File

@ -31,6 +31,7 @@ export interface IMessage {
countedAtt?: ITypeCount[]; countedAtt?: ITypeCount[];
transmission?: string; transmission?: string;
arg?: Arg[]; arg?: Arg[];
gifts?: IGift[];
r?: boolean; r?: boolean;
contextInfo?: string; contextInfo?: string;
acceptAction?: string; acceptAction?: string;
@ -43,6 +44,10 @@ export interface Arg {
Tag: string | number; Tag: string | number;
} }
export interface IGift {
GiftType: string;
}
//types are wrong //types are wrong
// export interface IMessageDatabase { // export interface IMessageDatabase {
// _id: Types.ObjectId; // _id: Types.ObjectId;
@ -80,6 +85,14 @@ export interface Arg {
// cinematic: string; // cinematic: string;
// requiredLevel: string; // requiredLevel: string;
// } // }
const giftSchema = new Schema<IGift>(
{
GiftType: String
},
{ _id: false }
);
const messageSchema = new Schema<IMessageDatabase>( const messageSchema = new Schema<IMessageDatabase>(
{ {
ownerId: Schema.Types.ObjectId, ownerId: Schema.Types.ObjectId,
@ -93,6 +106,7 @@ const messageSchema = new Schema<IMessageDatabase>(
endDate: Date, endDate: Date,
r: Boolean, r: Boolean,
att: { type: [String], default: undefined }, att: { type: [String], default: undefined },
gifts: { type: [giftSchema], default: undefined },
countedAtt: { type: [typeCountSchema], default: undefined }, countedAtt: { type: [typeCountSchema], default: undefined },
transmission: String, transmission: String,
arg: { arg: {

View File

@ -51,6 +51,7 @@ import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSe
import { getShipController } from "@/src/controllers/api/getShipController"; import { getShipController } from "@/src/controllers/api/getShipController";
import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController"; import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController";
import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController"; import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController";
import { giftingController } from "@/src/controllers/api/giftingController";
import { gildWeaponController } from "@/src/controllers/api/gildWeaponController"; import { gildWeaponController } from "@/src/controllers/api/gildWeaponController";
import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController"; import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController"; import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController";
@ -203,6 +204,7 @@ apiRouter.post("/getAlliance.php", getAllianceController);
apiRouter.post("/getFriends.php", getFriendsController); apiRouter.post("/getFriends.php", getFriendsController);
apiRouter.post("/getGuildDojo.php", getGuildDojoController); apiRouter.post("/getGuildDojo.php", getGuildDojoController);
apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController);
apiRouter.post("/gifting.php", giftingController);
apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/gildWeapon.php", gildWeaponController);
apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController); apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController);

View File

@ -100,3 +100,7 @@ export const getSuffixedName = (account: TAccountDocument): string => {
const suffix = ((crc32.str(name.toLowerCase() + "595") >>> 0) + platform_magics[platformId]) % 1000; const suffix = ((crc32.str(name.toLowerCase() + "595") >>> 0) + platform_magics[platformId]) % 1000;
return name + "#" + suffix.toString().padStart(3, "0"); return name + "#" + suffix.toString().padStart(3, "0");
}; };
export const getAccountFromSuffixedName = (name: string): Promise<TAccountDocument | null> => {
return Account.findOne({ DisplayName: name.split("#")[0] });
};