From 92e8ffd7099fc0328dd58c47d0a20d26fd3f1c8a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:04:21 -0700 Subject: [PATCH] feat: alliance invites (#1452) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1452 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/addToAllianceController.ts | 117 ++++++++++++++++++ src/controllers/api/addToGuildController.ts | 2 +- .../confirmAllianceInvitationController.ts | 37 ++++++ .../api/declineAllianceInviteController.ts | 17 +++ src/helpers/stringHelpers.ts | 18 +++ src/models/guildModel.ts | 8 +- src/routes/api.ts | 6 + src/types/guildTypes.ts | 2 - 8 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 src/controllers/api/addToAllianceController.ts create mode 100644 src/controllers/api/confirmAllianceInvitationController.ts create mode 100644 src/controllers/api/declineAllianceInviteController.ts diff --git a/src/controllers/api/addToAllianceController.ts b/src/controllers/api/addToAllianceController.ts new file mode 100644 index 00000000..e7b24dec --- /dev/null +++ b/src/controllers/api/addToAllianceController.ts @@ -0,0 +1,117 @@ +import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers"; +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { createMessage } from "@/src/services/inboxService"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; +import { GuildPermission } from "@/src/types/guildTypes"; +import { logger } from "@/src/utils/logger"; +import { RequestHandler } from "express"; +import { ExportFlavour } from "warframe-public-export-plus"; + +export const addToAllianceController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const account = await getAccountForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + // Check guild has invite permissions in the alliance + const allianceMember = (await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }))!; + if (!(allianceMember.Permissions & GuildPermission.Recruiter)) { + res.status(400).json({ Error: 104 }); + return; + } + + // Find clan to invite + const payload = getJSONfromString(String(req.body)); + const guilds = await Guild.find( + { + Name: + payload.clanName.indexOf("#") == -1 + ? new RegExp("^" + regexEscape(payload.clanName) + "#...$") + : payload.clanName + }, + "Name" + ); + if (guilds.length == 0) { + res.status(400).json({ Error: 101 }); + return; + } + if (guilds.length > 1) { + const choices: IGuildChoice[] = []; + for (const guild of guilds) { + choices.push({ + OriginalPlatform: 0, + Name: guild.Name + }); + } + res.json(choices); + return; + } + + // Add clan as a pending alliance member + try { + await AllianceMember.insertOne({ + allianceId: req.query.allianceId, + guildId: guilds[0]._id, + Pending: true, + Permissions: 0 + }); + } catch (e) { + logger.debug(`alliance invite failed due to ${String(e)}`); + res.status(400).json({ Error: 102 }); + return; + } + + // Send inbox message to founding warlord + // TOVERIFY: Should other warlords get this as well? + // TOVERIFY: Who/what should the sender be? + // TOVERIFY: Should this message be highPriority? + const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!; + const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType"); + const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!; + const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!; + await createMessage(invitedClanOwnerMember.accountId, [ + { + sndr: getSuffixedName(account), + msg: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Body", + arg: [ + { + Key: "THEIR_CLAN", + Tag: senderGuild.Name + }, + { + Key: "CLAN", + Tag: guilds[0].Name + }, + { + Key: "ALLIANCE", + Tag: alliance.Name + } + ], + sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title", + icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon, + contextInfo: alliance._id.toString(), + highPriority: true, + acceptAction: "ALLIANCE_INVITE", + declineAction: "ALLIANCE_INVITE", + hasAccountAction: true + } + ]); + + res.end(); +}; + +interface IAddToAllianceRequest { + clanName: string; +} + +interface IGuildChoice { + OriginalPlatform: number; + Name: string; +} diff --git a/src/controllers/api/addToGuildController.ts b/src/controllers/api/addToGuildController.ts index d2a89df4..c67b8d1a 100644 --- a/src/controllers/api/addToGuildController.ts +++ b/src/controllers/api/addToGuildController.ts @@ -88,7 +88,7 @@ export const addToGuildController: RequestHandler = async (req, res) => { RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable. }); } catch (e) { - logger.debug(`alliance invite failed due to ${String(e)}`); + logger.debug(`guild invite failed due to ${String(e)}`); res.status(400).send("Already requested"); } res.end(); diff --git a/src/controllers/api/confirmAllianceInvitationController.ts b/src/controllers/api/confirmAllianceInvitationController.ts new file mode 100644 index 00000000..8d998e77 --- /dev/null +++ b/src/controllers/api/confirmAllianceInvitationController.ts @@ -0,0 +1,37 @@ +import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; +import { getAllianceClient } from "@/src/services/guildService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const confirmAllianceInvitationController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const accountId = await getAccountIdForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + const allianceMember = await AllianceMember.findOne({ + allianceId: req.query.allianceId, + guildId: guildMember.guildId + }); + if (!allianceMember || !allianceMember.Pending) { + res.status(400); + return; + } + allianceMember.Pending = false; + + const guild = (await Guild.findById(guildMember.guildId))!; + guild.AllianceId = allianceMember.allianceId; + + await Promise.all([allianceMember.save(), guild.save()]); + + // Give client the new alliance data which uses "AllianceId" instead of "_id" in this response + const alliance = (await Alliance.findById(allianceMember.allianceId))!; + const { _id, ...rest } = await getAllianceClient(alliance, guild); + res.json({ + AllianceId: _id, + ...rest + }); +}; diff --git a/src/controllers/api/declineAllianceInviteController.ts b/src/controllers/api/declineAllianceInviteController.ts new file mode 100644 index 00000000..2d9f9dd6 --- /dev/null +++ b/src/controllers/api/declineAllianceInviteController.ts @@ -0,0 +1,17 @@ +import { AllianceMember, GuildMember } from "@/src/models/guildModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const declineAllianceInviteController: RequestHandler = async (req, res) => { + // Check requester is a warlord in their guild + const accountId = await getAccountIdForRequest(req); + const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!; + if (guildMember.rank > 1) { + res.status(400).json({ Error: 104 }); + return; + } + + await AllianceMember.deleteOne({ allianceId: req.query.allianceId, guildId: guildMember.guildId }); + + res.end(); +}; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index aee4355c..f24fab32 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -26,3 +26,21 @@ export const getIndexAfter = (str: string, searchWord: string): number => { } return index + searchWord.length; }; + +export const regexEscape = (str: string): string => { + str = str.split(".").join("\\."); + str = str.split("\\").join("\\\\"); + str = str.split("[").join("\\["); + str = str.split("]").join("\\]"); + str = str.split("+").join("\\+"); + str = str.split("*").join("\\*"); + str = str.split("$").join("\\$"); + str = str.split("^").join("\\^"); + str = str.split("?").join("\\?"); + str = str.split("|").join("\\|"); + str = str.split("(").join("\\("); + str = str.split(")").join("\\)"); + str = str.split("{").join("\\{"); + str = str.split("}").join("\\}"); + return str; +}; diff --git a/src/models/guildModel.ts b/src/models/guildModel.ts index cce21847..763e6241 100644 --- a/src/models/guildModel.ts +++ b/src/models/guildModel.ts @@ -265,10 +265,10 @@ 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 + allianceId: { type: Schema.Types.ObjectId, required: true }, + guildId: { type: Schema.Types.ObjectId, required: true }, + Pending: { type: Boolean, required: true }, + Permissions: { type: Number, required: true } }); allianceMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); diff --git a/src/routes/api.ts b/src/routes/api.ts index cadce506..82d93143 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -4,6 +4,7 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; +import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; @@ -18,6 +19,7 @@ import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/cla import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController"; import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; +import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController"; import { confirmGuildInvitationController } from "@/src/controllers/api/confirmGuildInvitationController"; import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; @@ -27,6 +29,7 @@ import { createGuildController } from "@/src/controllers/api/createGuildControll import { creditsController } from "@/src/controllers/api/creditsController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; +import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController"; import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; @@ -140,8 +143,10 @@ apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementControlle apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); +apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController); apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationController); apiRouter.get("/credits.php", creditsController); +apiRouter.get("/declineAllianceInvite.php", declineAllianceInviteController); apiRouter.get("/declineGuildInvite.php", declineGuildInviteController); apiRouter.get("/deleteSession.php", deleteSessionController); apiRouter.get("/dojo", dojoController); @@ -181,6 +186,7 @@ apiRouter.get("/updateSession.php", updateSessionGetController); apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/archonFusion.php", archonFusionController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 21cb16c6..d40c605c 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -315,8 +315,6 @@ export interface IAllianceMemberDatabase { 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