diff --git a/src/controllers/api/cancelGuildAdvertisementController.ts b/src/controllers/api/cancelGuildAdvertisementController.ts new file mode 100644 index 00000000..aa587201 --- /dev/null +++ b/src/controllers/api/cancelGuildAdvertisementController.ts @@ -0,0 +1,20 @@ +import { GuildAd } from "@/src/models/guildModel"; +import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = await getGuildForRequestEx(req, inventory); + if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) { + res.status(400).end(); + return; + } + + await GuildAd.deleteOne({ GuildId: guild._id }); + + res.end(); +}; diff --git a/src/controllers/api/contributeToDojoComponentController.ts b/src/controllers/api/contributeToDojoComponentController.ts index 3177d5b6..acdbf8a4 100644 --- a/src/controllers/api/contributeToDojoComponentController.ts +++ b/src/controllers/api/contributeToDojoComponentController.ts @@ -1,6 +1,7 @@ import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { + addGuildMemberMiscItemContribution, getDojoClient, getGuildForRequestEx, hasAccessToDojo, @@ -143,8 +144,7 @@ const processContribution = ( ItemCount: ingredientContribution.ItemCount * -1 }); - guildMember.MiscItemsContributed ??= []; - guildMember.MiscItemsContributed.push(ingredientContribution); + addGuildMemberMiscItemContribution(guildMember, ingredientContribution); } addMiscItems(inventory, miscItemChanges); inventoryChanges.MiscItems = miscItemChanges; diff --git a/src/controllers/api/contributeToVaultController.ts b/src/controllers/api/contributeToVaultController.ts index 58079e02..74dc91a4 100644 --- a/src/controllers/api/contributeToVaultController.ts +++ b/src/controllers/api/contributeToVaultController.ts @@ -1,5 +1,9 @@ import { GuildMember } from "@/src/models/guildModel"; -import { addVaultMiscItems, getGuildForRequestEx } from "@/src/services/guildService"; +import { + addGuildMemberMiscItemContribution, + addVaultMiscItems, + getGuildForRequestEx +} from "@/src/services/guildService"; import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -25,14 +29,8 @@ export const contributeToVaultController: RequestHandler = async (req, res) => { if (request.MiscItems.length) { addVaultMiscItems(guild, request.MiscItems); - guildMember.MiscItemsContributed ??= []; for (const item of request.MiscItems) { - const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType); - if (miscItemContribution) { - miscItemContribution.ItemCount += item.ItemCount; - } else { - guildMember.MiscItemsContributed.push(item); - } + addGuildMemberMiscItemContribution(guildMember, item); addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); } diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index 35b4d626..08909b08 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -1,5 +1,6 @@ import { RequestHandler } from "express"; import { + addGuildMemberMiscItemContribution, getGuildForRequestEx, getGuildVault, hasAccessToDojo, @@ -146,8 +147,7 @@ export const guildTechController: RequestHandler = async (req, res) => { ItemCount: miscItem.ItemCount * -1 }); - guildMember.MiscItemsContributed ??= []; - guildMember.MiscItemsContributed.push(miscItem); + addGuildMemberMiscItemContribution(guildMember, miscItem); } } addMiscItems(inventory, miscItemChanges); diff --git a/src/controllers/api/postGuildAdvertisementController.ts b/src/controllers/api/postGuildAdvertisementController.ts new file mode 100644 index 00000000..e33db645 --- /dev/null +++ b/src/controllers/api/postGuildAdvertisementController.ts @@ -0,0 +1,75 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { GuildAd, GuildMember } from "@/src/models/guildModel"; +import { + addGuildMemberMiscItemContribution, + addVaultMiscItems, + getGuildForRequestEx, + getVaultMiscItemCount, + hasGuildPermissionEx +} from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { IPurchaseParams } from "@/src/types/purchaseTypes"; +import { RequestHandler } from "express"; + +export const postGuildAdvertisementController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId MiscItems"); + const guild = await getGuildForRequestEx(req, inventory); + const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }))!; + if (!hasGuildPermissionEx(guild, guildMember, GuildPermission.Advertiser)) { + res.status(400).end(); + return; + } + const payload = getJSONfromString(String(req.body)); + + // Handle resource cost + const vendor = preprocessVendorManifest( + getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")! + ); + const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!; + if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) { + addVaultMiscItems(guild, [ + { + ItemType: offer.ItemPrices![0].ItemType, + ItemCount: offer.ItemPrices![0].ItemCount * -1 + } + ]); + } else { + const miscItem = inventory.MiscItems.find(x => x.ItemType == offer.ItemPrices![0].ItemType); + if (!miscItem || miscItem.ItemCount < offer.ItemPrices![0].ItemCount) { + res.status(400).json("Insufficient funds"); + return; + } + miscItem.ItemCount -= offer.ItemPrices![0].ItemCount; + addGuildMemberMiscItemContribution(guildMember, offer.ItemPrices![0]); + await guildMember.save(); + await inventory.save(); + } + + // Create or update ad + await GuildAd.findOneAndUpdate( + { GuildId: guild._id }, + { + Emblem: guild.Emblem, + Expiry: new Date(Date.now() + 12 * 3600 * 1000), + Features: payload.Features, + GuildName: guild.Name, + MemberCount: await GuildMember.countDocuments({ guildId: guild._id, status: 0 }), + RecruitMsg: payload.RecruitMsg, + Tier: guild.Tier + }, + { upsert: true } + ); + + res.end(); +}; + +interface IPostGuildAdvertisementRequest { + Features: number; + RecruitMsg: string; + Languages: string[]; + PurchaseParams: IPurchaseParams; +} diff --git a/src/controllers/dynamic/getGuildAdsController.ts b/src/controllers/dynamic/getGuildAdsController.ts new file mode 100644 index 00000000..1dbe8217 --- /dev/null +++ b/src/controllers/dynamic/getGuildAdsController.ts @@ -0,0 +1,26 @@ +import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; +import { GuildAd } from "@/src/models/guildModel"; +import { IGuildAdInfoClient } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const getGuildAdsController: RequestHandler = async (req, res) => { + const ads = await GuildAd.find(req.query.tier ? { Tier: req.query.tier } : {}); + const guildAdInfos: IGuildAdInfoClient[] = []; + for (const ad of ads) { + guildAdInfos.push({ + _id: toOid(ad.GuildId), + CrossPlatformEnabled: true, + Emblem: ad.Emblem, + Expiry: toMongoDate(ad.Expiry), + Features: ad.Features, + GuildName: ad.GuildName, + MemberCount: ad.MemberCount, + OriginalPlatform: 0, + RecruitMsg: ad.RecruitMsg, + Tier: ad.Tier + }); + } + res.json({ + GuildAdInfos: guildAdInfos + }); +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 11556893..fa880997 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -10,7 +10,8 @@ import { IGuildLogRoomChange, IGuildLogEntryRoster, IGuildLogEntryContributable, - IDojoLeaderboardEntry + IDojoLeaderboardEntry, + IGuildAdDatabase } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -165,6 +166,7 @@ const guildSchema = new Schema( Ranks: { type: [guildRankSchema], default: defaultRanks }, TradeTax: { type: Number, default: 0 }, Tier: { type: Number, default: 1 }, + Emblem: { type: Boolean }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, @@ -225,3 +227,19 @@ const guildMemberSchema = new Schema({ guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true }); export const GuildMember = model("GuildMember", guildMemberSchema); + +const guildAdSchema = new Schema({ + GuildId: { type: Schema.Types.ObjectId, required: true }, + Emblem: Boolean, + Expiry: { type: Date, required: true }, + Features: { type: Number, required: true }, + GuildName: { type: String, required: true }, + MemberCount: { type: Number, required: true }, + RecruitMsg: { type: String, required: true }, + Tier: { type: Number, required: true } +}); + +guildAdSchema.index({ guildId: 1 }, { unique: true }); +guildAdSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); + +export const GuildAd = model("GuildAd", guildAdSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index bb1404f5..970bc503 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -9,6 +9,7 @@ import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonContro import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { artifactsController } from "@/src/controllers/api/artifactsController"; import { artifactTransmutationController } from "@/src/controllers/api/artifactTransmutationController"; +import { cancelGuildAdvertisementController } from "@/src/controllers/api/cancelGuildAdvertisementController"; import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController"; import { changeGuildRankController } from "@/src/controllers/api/changeGuildRankController"; import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController"; @@ -80,6 +81,7 @@ import { nameWeaponController } from "@/src/controllers/api/nameWeaponController import { nemesisController } from "@/src/controllers/api/nemesisController"; import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; +import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { purchaseController } from "@/src/controllers/api/purchaseController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; @@ -131,6 +133,7 @@ const apiRouter = express.Router(); // get apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); +apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController); apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); @@ -228,6 +231,7 @@ apiRouter.post("/nameWeapon.php", nameWeaponController); apiRouter.post("/nemesis.php", nemesisController); apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); +apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController); apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); diff --git a/src/routes/dynamic.ts b/src/routes/dynamic.ts index 14185e22..fb24fe58 100644 --- a/src/routes/dynamic.ts +++ b/src/routes/dynamic.ts @@ -1,11 +1,13 @@ import express from "express"; import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController"; +import { getGuildAdsController } from "@/src/controllers/dynamic/getGuildAdsController"; import { getProfileViewingDataController } from "@/src/controllers/dynamic/getProfileViewingDataController"; import { worldStateController } from "@/src/controllers/dynamic/worldStateController"; const dynamicController = express.Router(); dynamicController.get("/aggregateSessions.php", aggregateSessionsController); +dynamicController.get("/getGuildAds.php", getGuildAdsController); dynamicController.get("/getProfileViewingData.php", getProfileViewingDataController); dynamicController.get("/worldState.php", worldStateController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 19bbf803..6b0339f0 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -298,6 +298,10 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon } }; +export const getVaultMiscItemCount = (guild: TGuildDatabaseDocument, itemType: string): number => { + return guild.VaultMiscItems?.find(x => x.ItemType == itemType)?.ItemCount ?? 0; +}; + export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITypeCount[]): void => { guild.VaultMiscItems ??= []; for (const miscItem of miscItems) { @@ -310,6 +314,16 @@ export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITyp } }; +export const addGuildMemberMiscItemContribution = (guildMember: IGuildMemberDatabase, item: ITypeCount): void => { + guildMember.MiscItemsContributed ??= []; + const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType); + if (miscItemContribution) { + miscItemContribution.ItemCount += item.ItemCount; + } else { + guildMember.MiscItemsContributed.push(item); + } +}; + export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { if (build.guildXpValue) { guild.ClaimedXP ??= []; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index ab4d1c06..a5a64f87 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -28,6 +28,7 @@ export interface IGuildDatabase { Ranks: IGuildRank[]; TradeTax: number; Tier: number; + Emblem?: boolean; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; @@ -223,3 +224,27 @@ export interface IDojoLeaderboardEntry { r: number; // rank n: string; // displayName } + +export interface IGuildAdInfoClient { + _id: IOid; // Guild ID + CrossPlatformEnabled: boolean; + Emblem?: boolean; + Expiry: IMongoDate; + Features: number; + GuildName: string; + MemberCount: number; + OriginalPlatform: number; + RecruitMsg: string; + Tier: number; +} + +export interface IGuildAdDatabase { + GuildId: Types.ObjectId; + Emblem?: boolean; + Expiry: Date; + Features: number; + GuildName: string; + MemberCount: number; + RecruitMsg: string; + Tier: number; +}