feat: batch remove friends (#2032)

Closes #1947

Reviewed-on: OpenWF/SpaceNinjaServer#2032
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-05-09 21:37:09 -07:00 committed by Sainan
parent ab32728c47
commit 31043b55de
2 changed files with 68 additions and 4 deletions

View File

@ -1,8 +1,13 @@
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel"; import { Friendship } from "@/src/models/friendModel";
import { Account } from "@/src/models/loginModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose";
export const removeFriendGetController: RequestHandler = async (req, res) => { export const removeFriendGetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -22,7 +27,7 @@ export const removeFriendGetController: RequestHandler = async (req, res) => {
await Promise.all(promises); await Promise.all(promises);
res.json({ res.json({
Friends: friends Friends: friends
}); } satisfies IRemoveFriendsResponse);
} else { } else {
const friendId = req.query.friendId as string; const friendId = req.query.friendId as string;
await Promise.all([ await Promise.all([
@ -30,7 +35,65 @@ export const removeFriendGetController: RequestHandler = async (req, res) => {
Friendship.deleteOne({ owner: friendId, friend: accountId }) Friendship.deleteOne({ owner: friendId, friend: accountId })
]); ]);
res.json({ res.json({
Friends: [{ $oid: friendId } satisfies IOid] Friends: [{ $oid: friendId }]
}); } satisfies IRemoveFriendsResponse);
} }
}; };
export const removeFriendPostController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IBatchRemoveFriendsRequest>(String(req.body));
const friends = new Set((await Friendship.find({ owner: accountId }, "friend")).map(x => x.friend));
// TOVERIFY: Should pending friendships also be kept?
// Keep friends that have been online within threshold
await parallelForeach([...friends], async friend => {
const account = (await Account.findById(friend, "LastLogin"))!;
const daysLoggedOut = (Date.now() - account.LastLogin.getTime()) / 86400_000;
if (daysLoggedOut < data.DaysLoggedOut) {
friends.delete(friend);
}
});
if (data.SkipClanmates) {
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
await parallelForeach([...friends], async friend => {
const friendInventory = await getInventory(friend.toString(), "GuildId");
if (friendInventory.GuildId?.equals(inventory.GuildId)) {
friends.delete(friend);
}
});
}
}
// Remove all remaining friends that aren't in SkipFriendIds & give response.
const promises = [];
const response: IOid[] = [];
for (const friend of friends) {
if (!data.SkipFriendIds.find(skipFriendId => checkFriendId(skipFriendId, friend))) {
promises.push(Friendship.deleteOne({ owner: accountId, friend: friend }));
promises.push(Friendship.deleteOne({ owner: friend, friend: accountId }));
response.push(toOid(friend));
}
}
await Promise.all(promises);
res.json({
Friends: response
} satisfies IRemoveFriendsResponse);
};
// The friend ids format is a bit weird, e.g. when 6633b81e9dba0b714f28ff02 (A) is friends with 67cdac105ef1f4b49741c267 (B), A's friend id for B is 808000105ef1f40560ca079e and B's friend id for A is 8000b81e9dba0b06408a8075.
const checkFriendId = (friendId: string, b: Types.ObjectId): boolean => {
return friendId.substring(6, 6 + 8) == b.toString().substring(6, 6 + 8);
};
interface IBatchRemoveFriendsRequest {
DaysLoggedOut: number;
SkipClanmates: boolean;
SkipFriendIds: string[];
}
interface IRemoveFriendsResponse {
Friends: IOid[];
}

View File

@ -103,7 +103,7 @@ import { questControlController } from "@/src/controllers/api/questControlContro
import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController";
import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController";
import { releasePetController } from "@/src/controllers/api/releasePetController"; import { releasePetController } from "@/src/controllers/api/releasePetController";
import { removeFriendGetController } from "@/src/controllers/api/removeFriendController"; import { removeFriendGetController, removeFriendPostController } from "@/src/controllers/api/removeFriendController";
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
@ -290,6 +290,7 @@ apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/questControl.php", questControlController); // U17 apiRouter.post("/questControl.php", questControlController); // U17
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/releasePet.php", releasePetController);
apiRouter.post("/removeFriend.php", removeFriendPostController);
apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/removeFromGuild.php", removeFromGuildController);
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController);