From 24ed580a972a60dc8809a544ae9eaba093f9feff Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 2 Apr 2025 04:59:21 -0700 Subject: [PATCH] feat: create alliance (#1423) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1423 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/createAllianceController.ts | 50 +++++++++++++++ src/controllers/api/getAllianceController.ts | 24 ++++++- src/controllers/api/getGuildController.ts | 6 +- src/models/guildModel.ts | 27 +++++++- src/routes/api.ts | 2 + src/services/guildService.ts | 38 ++++++++++- src/types/guildTypes.ts | 64 +++++++++++++++++-- 7 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 src/controllers/api/createAllianceController.ts diff --git a/src/controllers/api/createAllianceController.ts b/src/controllers/api/createAllianceController.ts new file mode 100644 index 00000000..e3a81a24 --- /dev/null +++ b/src/controllers/api/createAllianceController.ts @@ -0,0 +1,50 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAllianceClient } 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 createAllianceController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + const guild = (await Guild.findById(inventory.GuildId!, "Name Tier AllianceId"))!; + if (guild.AllianceId) { + res.status(400).send("Guild is already in an alliance").end(); + return; + } + const guildMember = (await GuildMember.findOne({ guildId: guild._id, accountId }, "rank"))!; + if (guildMember.rank > 1) { + res.status(400).send("Invalid permission").end(); + return; + } + const data = getJSONfromString(String(req.body)); + const alliance = new Alliance({ Name: data.allianceName }); + try { + await alliance.save(); + } catch (e) { + res.status(400).send("Alliance name already in use").end(); + return; + } + guild.AllianceId = alliance._id; + await Promise.all([ + guild.save(), + AllianceMember.insertOne({ + allianceId: alliance._id, + guildId: guild._id, + Pending: false, + Permissions: + GuildPermission.Ruler | + GuildPermission.Promoter | + GuildPermission.Recruiter | + GuildPermission.Treasurer | + GuildPermission.ChatModerator + }) + ]); + res.json(await getAllianceClient(alliance, guild)); +}; + +interface ICreateAllianceRequest { + allianceName: string; +} diff --git a/src/controllers/api/getAllianceController.ts b/src/controllers/api/getAllianceController.ts index 391dae5f..5da0966d 100644 --- a/src/controllers/api/getAllianceController.ts +++ b/src/controllers/api/getAllianceController.ts @@ -1,7 +1,25 @@ +import { Alliance, Guild } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; import { RequestHandler } from "express"; -const getAllianceController: RequestHandler = (_req, res) => { - res.sendStatus(200); +export const getAllianceController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "GuildId"); + if (inventory.GuildId) { + const guild = (await Guild.findById(inventory.GuildId, "Name Tier AllianceId"))!; + if (guild.AllianceId) { + const alliance = (await Alliance.findById(guild.AllianceId))!; + res.json(await getAllianceClient(alliance, guild)); + return; + } + } + res.end(); }; -export { getAllianceController }; +/*interface IGetAllianceRequest { + memberCount: number; + clanLeaderName: string; + clanLeaderId: string; +}*/ diff --git a/src/controllers/api/getGuildController.ts b/src/controllers/api/getGuildController.ts index 8a803bfe..b834c289 100644 --- a/src/controllers/api/getGuildController.ts +++ b/src/controllers/api/getGuildController.ts @@ -5,7 +5,7 @@ import { logger } from "@/src/utils/logger"; import { getInventory } from "@/src/services/inventoryService"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; -const getGuildController: RequestHandler = async (req, res) => { +export const getGuildController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const inventory = await getInventory(accountId, "GuildId"); if (inventory.GuildId) { @@ -28,7 +28,5 @@ const getGuildController: RequestHandler = async (req, res) => { return; } } - res.sendStatus(200); + res.end(); }; - -export { getGuildController }; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index 0ce72882..ecc01151 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -11,7 +11,9 @@ import { IGuildLogEntryRoster, IGuildLogEntryContributable, IDojoLeaderboardEntry, - IGuildAdDatabase + IGuildAdDatabase, + IAllianceDatabase, + IAllianceMemberDatabase } from "@/src/types/guildTypes"; import { Document, Model, model, Schema, Types } from "mongoose"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; @@ -167,6 +169,7 @@ const guildSchema = new Schema( TradeTax: { type: Number, default: 0 }, Tier: { type: Number, default: 1 }, Emblem: { type: Boolean }, + AllianceId: { type: Types.ObjectId }, DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoCapacity: { type: Number, default: 100 }, DojoEnergy: { type: Number, default: 5 }, @@ -246,3 +249,25 @@ guildAdSchema.index({ GuildId: 1 }, { unique: true }); guildAdSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); export const GuildAd = model("GuildAd", guildAdSchema); + +const allianceSchema = new Schema({ + Name: String, + MOTD: longMOTDSchema, + LongMOTD: longMOTDSchema, + Emblem: Boolean +}); + +allianceSchema.index({ Name: 1 }, { unique: true }); + +export const Alliance = model("Alliance", allianceSchema); + +const allianceMemberSchema = new Schema({ + allianceId: Schema.Types.ObjectId, + guildId: Schema.Types.ObjectId, + Pending: Boolean, + Permissions: Number +}); + +guildMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); + +export const AllianceMember = model("AllianceMember", allianceMemberSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index 9845e42b..979090f4 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -22,6 +22,7 @@ import { confirmGuildInvitationController } from "@/src/controllers/api/confirmG import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; import { contributeToVaultController } from "@/src/controllers/api/contributeToVaultController"; +import { createAllianceController } from "@/src/controllers/api/createAllianceController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; @@ -193,6 +194,7 @@ apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationController); apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentController); apiRouter.post("/contributeToVault.php", contributeToVaultController); +apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); diff --git a/src/services/guildService.ts b/src/services/guildService.ts index 1da4a370..22b42c18 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -1,10 +1,13 @@ import { Request } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory } from "@/src/services/inventoryService"; -import { Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; +import { AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { GuildPermission, + IAllianceClient, + IAllianceDatabase, + IAllianceMemberClient, IDojoClient, IDojoComponentClient, IDojoComponentDatabase, @@ -99,7 +102,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s XP: guild.XP, IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)), NumContributors: guild.CeremonyContributors?.length ?? 0, - CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined + CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, + AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined }; }; @@ -549,4 +553,34 @@ export const deleteGuild = async (guildId: Types.ObjectId): Promise => { }); await GuildAd.deleteOne({ GuildId: guildId }); + + await AllianceMember.deleteMany({ guildId }); + + // TODO: If this guild was the founding guild of an alliance (ruler permission), that would need to be forcefully deleted now as well. +}; + +export const getAllianceClient = async ( + alliance: IAllianceDatabase, + guild: TGuildDatabaseDocument +): Promise => { + const allianceMembers = await AllianceMember.find({ allianceId: alliance._id }); + const clans: IAllianceMemberClient[] = []; + for (const allianceMember of allianceMembers) { + const memberGuild = allianceMember.guildId.equals(guild._id) + ? guild + : (await Guild.findById(allianceMember.guildId))!; + clans.push({ + _id: toOid(allianceMember.guildId), + Name: memberGuild.Name, + Tier: memberGuild.Tier, + Pending: allianceMember.Pending, + Permissions: allianceMember.Permissions, + MemberCount: await GuildMember.countDocuments({ guildId: memberGuild._id, status: 0 }) + }); + } + return { + _id: toOid(alliance._id), + Name: alliance.Name, + Clans: clans + }; }; diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 1ff37923..a4b706fa 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -18,6 +18,9 @@ export interface IGuildClient { IsContributor: boolean; NumContributors: number; CeremonyResetDate?: IMongoDate; + CrossPlatformEnabled?: boolean; + AutoContributeFromVault?: boolean; + AllianceId?: IOid; } export interface IGuildDatabase { @@ -29,6 +32,7 @@ export interface IGuildDatabase { TradeTax: number; Tier: number; Emblem?: boolean; + AllianceId?: Types.ObjectId; DojoComponents: IDojoComponentDatabase[]; DojoCapacity: number; @@ -60,21 +64,21 @@ export interface IGuildDatabase { export interface ILongMOTD { message: string; authorName: string; - //authorGuildName: ""; + authorGuildName?: ""; } // 32 seems to be reserved export enum GuildPermission { - Ruler = 1, // Change clan hierarchy + Ruler = 1, // Clan: Change hierarchy. Alliance: Kick clans. Advertiser = 8192, - Recruiter = 2, // Invite members + Recruiter = 2, // Send invites (Clans & Alliances) Regulator = 4, // Kick members - Promoter = 8, // Promote and demote members + Promoter = 8, // Clan: Promote and demote members. Alliance: Change clan permissions. Architect = 16, // Create and destroy rooms Decorator = 1024, // Create and destroy decos - Treasurer = 64, // Contribute from vault and edit tax rate + Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault. Tech = 128, // Queue research - ChatModerator = 512, + ChatModerator = 512, // (Clans & Alliances) Herald = 2048, // Change MOTD Fabricator = 4096 // Replicate research } @@ -268,3 +272,51 @@ export interface IGuildAdDatabase { RecruitMsg: string; Tier: number; } + +export interface IAllianceClient { + _id: IOid; + Name: string; + MOTD?: ILongMOTD; + LongMOTD?: ILongMOTD; + Emblem?: boolean; + CrossPlatformEnabled?: boolean; + Clans: IAllianceMemberClient[]; + OriginalPlatform?: number; +} + +export interface IAllianceDatabase { + _id: Types.ObjectId; + Name: string; + MOTD?: ILongMOTD; + LongMOTD?: ILongMOTD; + Emblem?: boolean; +} + +export interface IAllianceMemberClient { + _id: IOid; + Name: string; + Tier: number; + Pending: boolean; + Emblem?: boolean; + Permissions: number; + MemberCount: number; + ClanLeader?: string; + ClanLeaderId?: IOid; + OriginalPlatform?: number; +} + +export interface IAllianceMemberDatabase { + allianceId: Types.ObjectId; + guildId: Types.ObjectId; + Pending: boolean; + Permissions: number; +} + +// TODO: Alliance chat permissions +// TODO: POST /api/addToAlliance.php: {"clanName":"abc"} +// TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=1 +// TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=0 +// TODO: GET /api/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0 +// TODO: GET /api/setAllianceGuildPermissions.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=000000000000000000000042&perms=2 +// TODO: Handle alliance in contributeToVault +// TODO: Handle alliance in setGuildMotd