返回提交历史
Modified
src/controllers/dynamic/getProfileViewingDataController.ts
+128
-119
Modified
src/controllers/stats/viewController.ts
+15
-3
XFEstudio/XFESpaceNinjaServer
feat: handle guild profile requests for old versions (#3293)
Closes #3291 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3293 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
267a2708
代码差异
2 个文件
+143
-122
@@ -5,7 +5,6 @@ import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/in
5
5
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
6
6
import { Loadout } from "../../models/inventoryModels/loadoutModel.ts";
7
7
import { Account } from "../../models/loginModel.ts";
8
import type { TStatsDatabaseDocument } from "../../models/statsModel.ts";
9
8
import { Stats } from "../../models/statsModel.ts";
10
9
import { allDailyAffiliationKeys } from "../../services/inventoryService.ts";
11
10
import type { IMongoDate, IOid } from "../../types/commonTypes.ts";
@@ -25,12 +24,54 @@ import { getJSONfromString } from "../../helpers/stringHelpers.ts";
25
24
import { ExportDojoRecipes } from "warframe-public-export-plus";
26
25
import type { IStatsClient } from "../../types/statTypes.ts";
27
26
import { toStoreItem } from "../../services/itemDataService.ts";
28
import type { FlattenMaps } from "mongoose";
29
27
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
30
28
import type { ILoadoutConfigClient } from "../../types/saveLoadoutTypes.ts";
31
29
import { skinLookupTable } from "../../helpers/skinLookupTable.ts";
30
import type { ITechProjectClient } from "../../types/guildTypes.ts";
32
31
33
const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise<IProfileViewingData | undefined> => {
32
export const getProfileViewingDataGetController: RequestHandler = async (req, res) => {
33
if (req.query.playerId) {
34
const data = await getProfileViewingDataByPlayerId(req.query.playerId as string);
35
if (data) {
36
res.json(data);
37
} else {
38
res.status(409).send("Could not find requested account");
39
}
40
} else if (req.query.guildId) {
41
const data = await getProfileViewingDataByGuildId(req.query.guildId as string);
42
if (data) {
43
res.json(data);
44
} else {
45
res.status(409).send("Could not find guild");
46
}
47
} else {
48
res.sendStatus(400);
49
}
50
};
51
52
// For old versions, this was an authenticated POST request.
53
type IGetProfileViewingDataRequest = { AccountId: string } | { GuildId: string };
54
export const getProfileViewingDataPostController: RequestHandler = async (req, res) => {
55
const payload = getJSONfromString<IGetProfileViewingDataRequest>(String(req.body));
56
if ("GuildId" in payload) {
57
const data = await getProfileViewingDataByGuildId(payload.GuildId);
58
if (data) {
59
res.json(data);
60
} else {
61
res.status(409).send("Could not find guild");
62
}
63
} else {
64
const playerId = req.query.playerId as string; // companion app sends a POST request should be handled like a GET request
65
const data = await getProfileViewingDataByPlayerId(playerId ? playerId : payload.AccountId);
66
if (data) {
67
res.json(data);
68
} else {
69
res.status(409).send("Could not find requested account");
70
}
71
}
72
};
73
74
export const getProfileViewingDataByPlayerId = async (playerId: string): Promise<IProfileViewingData | undefined> => {
34
75
const account = await Account.findById(playerId, "DisplayName");
35
76
if (!account) {
36
77
return;
@@ -77,148 +118,116 @@ const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise<IP
77
118
result[key] = inventory[key];
78
119
}
79
120
80
const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON<Partial<TStatsDatabaseDocument>>();
81
delete stats._id;
82
delete stats.__v;
83
delete stats.accountOwnerId;
84
85
121
return {
86
122
Results: [result],
87
123
TechProjects: [],
88
124
XpComponents: [],
89
125
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
90
Stats: stats
126
Stats: (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON() as IStatsClient
91
127
};
92
128
};
93
129
94
export const getProfileViewingDataGetController: RequestHandler = async (req, res) => {
95
if (req.query.playerId) {
96
const data = await getProfileViewingDataByPlayerIdImpl(req.query.playerId as string);
97
if (data) {
98
res.json(data);
99
} else {
100
res.status(409).send("Could not find requested account");
101
}
102
} else if (req.query.guildId) {
103
const guild = await Guild.findById(
104
req.query.guildId as string,
105
"Name Tier XP Class Emblem TechProjects ClaimedXP"
106
);
107
if (!guild) {
108
res.status(409).send("Could not find guild");
109
return;
110
}
111
const members = await GuildMember.find({ guildId: guild._id, status: 0 });
112
const results: IPlayerProfileViewingDataResult[] = [];
113
for (let i = 0; i != Math.min(4, members.length); ++i) {
114
const member = members[i];
115
const [account, inventory] = await Promise.all([
116
Account.findById(member.accountId, "DisplayName"),
117
Inventory.findOne(
118
{ accountOwnerId: member.accountId },
119
"DisplayName PlayerLevel XPInfo LoadOutPresets CurrentLoadOutIds WeaponSkins Suits Pistols LongGuns Melee"
120
)
121
]);
122
const result: IPlayerProfileViewingDataResult = {
123
AccountId: toOid(account!._id),
124
DisplayName: account!.DisplayName,
125
PlayerLevel: inventory!.PlayerLevel,
126
LoadOutInventory: {
127
WeaponSkins: [],
128
XPInfo: inventory!.XPInfo
129
}
130
};
131
await populateLoadout(inventory!, result);
132
results.push(result);
133
}
134
populateGuild(guild, results[0]);
130
export const getProfileViewingDataByGuildId = async (guildId: string): Promise<IProfileViewingData | undefined> => {
131
const guild = await Guild.findById(guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP");
132
if (!guild) {
133
return;
134
}
135
const members = await GuildMember.find({ guildId: guild._id, status: 0 });
136
const results: IPlayerProfileViewingDataResult[] = [];
137
for (let i = 0; i != Math.min(4, members.length); ++i) {
138
const member = members[i];
139
const [account, inventory] = await Promise.all([
140
Account.findById(member.accountId, "DisplayName"),
141
Inventory.findOne(
142
{ accountOwnerId: member.accountId },
143
"DisplayName PlayerLevel XPInfo LoadOutPresets CurrentLoadOutIds WeaponSkins Suits Pistols LongGuns Melee"
144
)
145
]);
146
const result: IPlayerProfileViewingDataResult = {
147
AccountId: toOid(account!._id),
148
DisplayName: account!.DisplayName,
149
PlayerLevel: inventory!.PlayerLevel,
150
LoadOutInventory: {
151
WeaponSkins: [],
152
XPInfo: inventory!.XPInfo
153
}
154
};
155
await populateLoadout(inventory!, result);
156
results.push(result);
157
}
158
populateGuild(guild, results[0]);
135
159
136
const combinedStats: IStatsClient = {};
137
const statsArr = await Stats.find({ accountOwnerId: { $in: members.map(x => x.accountId) } }).lean(); // need this as POJO so Object.entries works as expected
138
for (const stats of statsArr) {
139
for (const [key, value] of Object.entries(stats)) {
140
if (typeof value == "number" && key != "__v") {
141
(combinedStats[key as keyof IStatsClient] as number | undefined) ??= 0;
142
(combinedStats[key as keyof IStatsClient] as number) += value;
143
}
160
const combinedStats: IStatsClient = {};
161
const statsArr = await Stats.find({ accountOwnerId: { $in: members.map(x => x.accountId) } }).lean(); // need this as POJO so Object.entries works as expected
162
for (const stats of statsArr) {
163
for (const [key, value] of Object.entries(stats)) {
164
if (typeof value == "number" && key != "__v") {
165
(combinedStats[key as keyof IStatsClient] as number | undefined) ??= 0;
166
(combinedStats[key as keyof IStatsClient] as number) += value;
144
167
}
145
for (const arrayName of ["Weapons", "Enemies", "Scans", "Missions", "PVP"] as const) {
146
if (stats[arrayName]) {
147
combinedStats[arrayName] ??= [];
148
for (const entry of stats[arrayName]) {
149
const combinedEntry = combinedStats[arrayName].find(x => x.type == entry.type);
150
if (combinedEntry) {
151
for (const [key, value] of Object.entries(entry)) {
152
if (typeof value == "number") {
153
(combinedEntry[key as keyof typeof combinedEntry] as unknown as
154
| number
155
| undefined) ??= 0;
156
(combinedEntry[key as keyof typeof combinedEntry] as unknown as number) += value;
157
}
168
}
169
for (const arrayName of ["Weapons", "Enemies", "Scans", "Missions", "PVP"] as const) {
170
if (stats[arrayName]) {
171
combinedStats[arrayName] ??= [];
172
for (const entry of stats[arrayName]) {
173
const combinedEntry = combinedStats[arrayName].find(x => x.type == entry.type);
174
if (combinedEntry) {
175
for (const [key, value] of Object.entries(entry)) {
176
if (typeof value == "number") {
177
(combinedEntry[key as keyof typeof combinedEntry] as unknown as number | undefined) ??=
178
0;
179
(combinedEntry[key as keyof typeof combinedEntry] as unknown as number) += value;
158
180
}
159
} else {
160
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
161
combinedStats[arrayName].push(entry as any);
162
181
}
182
} else {
183
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
184
combinedStats[arrayName].push(entry as any);
163
185
}
164
186
}
165
187
}
166
188
}
189
}
167
190
168
const xpComponents: IXPComponentClient[] = [];
169
if (guild.ClaimedXP) {
170
for (const componentName of guild.ClaimedXP) {
171
if (componentName.endsWith(".level")) {
172
const [key] = Object.entries(ExportDojoRecipes.rooms).find(
173
([_key, value]) => value.resultType == componentName
174
)!;
175
xpComponents.push({
176
StoreTypeName: toStoreItem(key)
177
});
178
} else {
179
const [key] = Object.entries(ExportDojoRecipes.decos).find(
180
([_key, value]) => value.resultType == componentName
181
)!;
182
xpComponents.push({
183
StoreTypeName: toStoreItem(key)
184
});
185
}
191
const xpComponents: IXPComponentClient[] = [];
192
if (guild.ClaimedXP) {
193
for (const componentName of guild.ClaimedXP) {
194
if (componentName.endsWith(".level")) {
195
const [key] = Object.entries(ExportDojoRecipes.rooms).find(
196
([_key, value]) => value.resultType == componentName
197
)!;
198
xpComponents.push({
199
StoreTypeName: toStoreItem(key)
200
});
201
} else {
202
const [key] = Object.entries(ExportDojoRecipes.decos).find(
203
([_key, value]) => value.resultType == componentName
204
)!;
205
xpComponents.push({
206
StoreTypeName: toStoreItem(key)
207
});
186
208
}
187
209
}
188
189
res.json({
190
Results: results,
191
TechProjects: guild.TechProjects,
192
XpComponents: xpComponents,
193
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
194
Stats: combinedStats
195
});
196
} else {
197
res.sendStatus(400);
198
210
}
199
};
200
211
201
// For old versions, this was an authenticated POST request.
202
interface IGetProfileViewingDataRequest {
203
AccountId: string;
204
}
205
export const getProfileViewingDataPostController: RequestHandler = async (req, res) => {
206
const payload = getJSONfromString<IGetProfileViewingDataRequest>(String(req.body));
207
const playerId = req.query.playerId as string; // companion app sends a POST request should be handled like a GET request
208
const data = await getProfileViewingDataByPlayerIdImpl(playerId ? playerId : payload.AccountId);
209
if (data) {
210
res.json(data);
211
} else {
212
res.status(409).send("Could not find requested account");
213
}
212
return {
213
Results: results,
214
TechProjects:
215
guild.TechProjects?.map(x => ({
216
...x,
217
CompletionDate: x.CompletionDate ? toMongoDate(x.CompletionDate) : undefined
218
})) ?? [],
219
XpComponents: xpComponents,
220
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
221
Stats: combinedStats
222
};
214
223
};
215
224
216
225
interface IProfileViewingData {
217
226
Results: IPlayerProfileViewingDataResult[];
218
TechProjects: [];
219
XpComponents: [];
227
TechProjects: ITechProjectClient[];
228
XpComponents: IXPComponentClient[];
220
229
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
221
Stats: FlattenMaps<Partial<TStatsDatabaseDocument>>;
230
Stats: IStatsClient;
222
231
}
223
232
224
233
interface IPlayerProfileViewingDataResult extends Partial<IDailyAffiliations>, IInventoryAccolades {
@@ -2,11 +2,23 @@ import type { RequestHandler } from "express";
2
2
import { getInventory } from "../../services/inventoryService.ts";
3
3
import { getStats } from "../../services/statsService.ts";
4
4
import type { IStatsClient } from "../../types/statTypes.ts";
5
import { getProfileViewingDataByGuildId } from "../dynamic/getProfileViewingDataController.ts";
5
6
6
7
const viewController: RequestHandler = async (req, res) => {
7
const accountId = String(req.query.id ?? req.query.lookupId);
8
const inventory = await getInventory(accountId, "XPInfo");
9
const playerStats = await getStats(accountId);
8
const lookupId = String(req.query.id ?? req.query.lookupId);
9
10
if (req.query.guild == "1") {
11
const data = await getProfileViewingDataByGuildId(lookupId);
12
if (data) {
13
res.json(data.Stats);
14
} else {
15
res.status(409).send("Could not find guild");
16
}
17
return;
18
}
19
20
const inventory = await getInventory(lookupId, "XPInfo");
21
const playerStats = await getStats(lookupId);
10
22
11
23
const responseJson = playerStats.toJSON<IStatsClient>();
12
24
responseJson.Weapons ??= [];