feat: alliance invites (#1452)

Reviewed-on: OpenWF/SpaceNinjaServer#1452
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-04-03 19:04:21 -07:00 committed by Sainan
parent 0b18932dd8
commit 92e8ffd709
8 changed files with 200 additions and 7 deletions

View File

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

View File

@ -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. 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) { } 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.status(400).send("Already requested");
} }
res.end(); res.end();

View File

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

View File

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

View File

@ -26,3 +26,21 @@ export const getIndexAfter = (str: string, searchWord: string): number => {
} }
return index + searchWord.length; 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;
};

View File

@ -265,10 +265,10 @@ allianceSchema.index({ Name: 1 }, { unique: true });
export const Alliance = model<IAllianceDatabase>("Alliance", allianceSchema); export const Alliance = model<IAllianceDatabase>("Alliance", allianceSchema);
const allianceMemberSchema = new Schema<IAllianceMemberDatabase>({ const allianceMemberSchema = new Schema<IAllianceMemberDatabase>({
allianceId: Schema.Types.ObjectId, allianceId: { type: Schema.Types.ObjectId, required: true },
guildId: Schema.Types.ObjectId, guildId: { type: Schema.Types.ObjectId, required: true },
Pending: Boolean, Pending: { type: Boolean, required: true },
Permissions: Number Permissions: { type: Number, required: true }
}); });
allianceMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true }); allianceMemberSchema.index({ allianceId: 1, guildId: 1 }, { unique: true });

View File

@ -4,6 +4,7 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom
import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController";
import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController";
import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController";
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController";
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
import { archonFusionController } from "@/src/controllers/api/archonFusionController"; 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 { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController";
import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController"; import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController";
import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController"; import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController";
import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController";
import { confirmGuildInvitationController } from "@/src/controllers/api/confirmGuildInvitationController"; import { confirmGuildInvitationController } from "@/src/controllers/api/confirmGuildInvitationController";
import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController"; import { contributeGuildClassController } from "@/src/controllers/api/contributeGuildClassController";
import { contributeToDojoComponentController } from "@/src/controllers/api/contributeToDojoComponentController"; 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 { creditsController } from "@/src/controllers/api/creditsController";
import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController";
import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController";
import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController";
import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController"; import { declineGuildInviteController } from "@/src/controllers/api/declineGuildInviteController";
import { deleteSessionController } from "@/src/controllers/api/deleteSessionController"; import { deleteSessionController } from "@/src/controllers/api/deleteSessionController";
import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController"; import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController";
@ -140,8 +143,10 @@ apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementControlle
apiRouter.get("/changeGuildRank.php", changeGuildRankController); apiRouter.get("/changeGuildRank.php", changeGuildRankController);
apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController); apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController);
apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController); apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController);
apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController);
apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationController); apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationController);
apiRouter.get("/credits.php", creditsController); apiRouter.get("/credits.php", creditsController);
apiRouter.get("/declineAllianceInvite.php", declineAllianceInviteController);
apiRouter.get("/declineGuildInvite.php", declineGuildInviteController); apiRouter.get("/declineGuildInvite.php", declineGuildInviteController);
apiRouter.get("/deleteSession.php", deleteSessionController); apiRouter.get("/deleteSession.php", deleteSessionController);
apiRouter.get("/dojo", dojoController); apiRouter.get("/dojo", dojoController);
@ -181,6 +186,7 @@ apiRouter.get("/updateSession.php", updateSessionGetController);
apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/abortDojoComponent.php", abortDojoComponentController);
apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/activateRandomMod.php", activateRandomModController);
apiRouter.post("/addFriendImage.php", addFriendImageController); apiRouter.post("/addFriendImage.php", addFriendImageController);
apiRouter.post("/addToAlliance.php", addToAllianceController);
apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/addToGuild.php", addToGuildController);
apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/arcaneCommon.php", arcaneCommonController);
apiRouter.post("/archonFusion.php", archonFusionController); apiRouter.post("/archonFusion.php", archonFusionController);

View File

@ -315,8 +315,6 @@ export interface IAllianceMemberDatabase {
Permissions: number; 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=1
// TODO: GET /api/divvyAllianceVault.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0&allianceId=000000000000000000000069&credits=0 // 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/removeFromAlliance.php?accountId=6633b81e9dba0b714f28ff02&nonce=5702391171614479&ct=MSI&guildId=663e9be9f741eeb5782f9df0