feat: guild ads (#1390)
Some checks failed
Build / build (22) (push) Waiting to run
Build / build (18) (push) Has been cancelled
Build / build (20) (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled

Reviewed-on: #1390
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-31 04:14:00 -07:00 committed by Sainan
parent 01f04c287a
commit 48598c145f
11 changed files with 198 additions and 14 deletions

View File

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

View File

@ -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;

View File

@ -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 }]);
}

View File

@ -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);

View File

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

View File

@ -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
});
};

View File

@ -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<IGuildDatabase>(
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<IGuildMemberDatabase>({
guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true });
export const GuildMember = model<IGuildMemberDatabase>("GuildMember", guildMemberSchema);
const guildAdSchema = new Schema<IGuildAdDatabase>({
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<IGuildAdDatabase>("GuildAd", guildAdSchema);

View File

@ -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);

View File

@ -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);

View File

@ -1,7 +1,7 @@
import { Request } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addRecipes, getInventory } from "@/src/services/inventoryService";
import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
GuildPermission,
@ -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 ??= [];
@ -513,4 +527,6 @@ export const deleteGuild = async (guildId: Types.ObjectId): Promise<void> => {
contextInfo: guildId.toString(),
acceptAction: "GUILD_INVITE"
});
await GuildAd.deleteOne({ GuildId: guildId });
};

View File

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