返回提交历史
Modified
.eslintrc
+3
-1
Modified
src/controllers/api/inboxController.ts
+80
-5
Modified
src/controllers/api/loginController.ts
+3
-2
Added
src/controllers/custom/createMessageController.ts
+14
-0
Modified
src/helpers/customHelpers/customHelpers.ts
+2
-1
Added
src/models/inboxModel.ts
+130
-0
Modified
src/models/inventoryModels/inventoryModel.ts
+8
-11
Modified
src/models/loginModel.ts
+2
-16
Modified
src/routes/custom.ts
+2
-0
Added
src/services/inboxService.ts
+66
-0
Modified
src/services/inventoryService.ts
+12
-0
Modified
src/types/inventoryTypes/inventoryTypes.ts
+10
-6
Modified
src/types/loginTypes.ts
+22
-21
Renamed
static/fixed_responses/messages.json
+13
-7
XFEstudio/SpaceNinjaServer
feat: Inbox (#876)
50c280cf
代码差异
14 个文件
+367
-70
@@ -26,7 +26,9 @@
26
26
"no-case-declarations": "warn",
27
27
"prettier/prettier": "error",
28
28
"@typescript-eslint/semi": "error",
29
"no-mixed-spaces-and-tabs": "error"
29
"no-mixed-spaces-and-tabs": "error",
30
"require-await": "off",
31
"@typescript-eslint/require-await": "error"
30
32
},
31
33
"parser": "@typescript-eslint/parser",
32
34
"parserOptions": {
@@ -1,8 +1,83 @@
1
1
import { RequestHandler } from "express";
2
import inbox from "@/static/fixed_responses/inbox.json";
2
import { Inbox } from "@/src/models/inboxModel";
3
import {
4
createNewEventMessages,
5
deleteAllMessagesRead,
6
deleteMessageRead,
7
getAllMessagesSorted,
8
getMessage
9
} from "@/src/services/inboxService";
10
import { getAccountIdForRequest } from "@/src/services/loginService";
11
import { addItems, getInventory } from "@/src/services/inventoryService";
12
import { logger } from "@/src/utils/logger";
3
13
4
const inboxController: RequestHandler = (_req, res) => {
5
res.json(inbox);
6
};
14
export const inboxController: RequestHandler = async (req, res) => {
15
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
16
17
const accountId = await getAccountIdForRequest(req);
18
19
if (deleteId) {
20
if (deleteId === "DeleteAllRead") {
21
await deleteAllMessagesRead(accountId);
22
res.status(200).end();
23
return;
24
}
25
26
await deleteMessageRead(deleteId as string);
27
res.status(200).end();
28
} else if (messageId) {
29
const message = await getMessage(messageId as string);
30
message.r = true;
31
const attachmentItems = message.att;
32
const attachmentCountedItems = message.countedAtt;
33
34
if (!attachmentItems && !attachmentCountedItems) {
35
await message.save();
36
37
res.status(200).end();
38
return;
39
}
7
40
8
export { inboxController };
41
const inventory = await getInventory(accountId);
42
const inventoryChanges = {};
43
if (attachmentItems) {
44
await addItems(
45
inventory,
46
attachmentItems.map(attItem => ({ ItemType: attItem, ItemCount: 1 })),
47
inventoryChanges
48
);
49
}
50
if (attachmentCountedItems) {
51
await addItems(inventory, attachmentCountedItems, inventoryChanges);
52
}
53
await inventory.save();
54
await message.save();
55
56
res.json({ InventoryChanges: inventoryChanges });
57
} else if (latestClientMessageId) {
58
await createNewEventMessages(req);
59
const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 });
60
61
const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId);
62
63
if (!latestClientMessage) {
64
logger.debug(`this should only happen after DeleteAllRead `);
65
res.json({ Inbox: messages });
66
return;
67
}
68
const newMessages = messages.filter(m => m.date > latestClientMessage.date);
69
70
if (newMessages.length === 0) {
71
res.send("no-new");
72
return;
73
}
74
75
res.json({ Inbox: newMessages });
76
} else {
77
//newly created event messages must be newer than account.LatestEventMessageDate
78
await createNewEventMessages(req);
79
const messages = await getAllMessagesSorted(accountId);
80
const inbox = messages.map(m => m.toJSON());
81
res.json({ Inbox: inbox });
82
}
83
};
@@ -12,7 +12,7 @@ import { logger } from "@/src/utils/logger";
12
12
export const loginController: RequestHandler = async (request, response) => {
13
13
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
14
14
15
const account = await Account.findOne({ email: loginRequest.email }); //{ _id: 0, __v: 0 }
15
const account = await Account.findOne({ email: loginRequest.email });
16
16
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
17
17
18
18
const buildLabel: string =
@@ -41,7 +41,8 @@ export const loginController: RequestHandler = async (request, response) => {
41
41
ForceLogoutVersion: 0,
42
42
ConsentNeeded: false,
43
43
TrackedSettings: [],
44
Nonce: nonce
44
Nonce: nonce,
45
LatestEventMessageDate: new Date(0)
45
46
});
46
47
logger.debug("created new account");
47
48
response.json(createLoginResponse(newAccount, buildLabel));
@@ -0,0 +1,14 @@
1
import { createMessage, IMessageCreationTemplate } from "@/src/services/inboxService";
2
import { RequestHandler } from "express";
3
4
export const createMessageController: RequestHandler = async (req, res) => {
5
const message = req.body as (IMessageCreationTemplate & { ownerId: string })[] | undefined;
6
7
if (!message) {
8
res.status(400).send("No message provided");
9
return;
10
}
11
const savedMessages = await createMessage(message[0].ownerId, message);
12
13
res.json(savedMessages);
14
};
@@ -48,7 +48,8 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccount =>
48
48
CrossPlatformAllowed: true,
49
49
ForceLogoutVersion: 0,
50
50
TrackedSettings: [],
51
Nonce: 0
51
Nonce: 0,
52
LatestEventMessageDate: new Date(0)
52
53
} satisfies IDatabaseAccount;
53
54
};
54
55
@@ -0,0 +1,130 @@
1
import { model, Schema, Types } from "mongoose";
2
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
3
import { typeCountSchema } from "@/src/models/inventoryModels/inventoryModel";
4
import { IMongoDate, IOid } from "@/src/types/commonTypes";
5
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
6
7
export interface IMessageClient extends Omit<IMessageDatabase, "_id" | "date" | "startDate" | "endDate" | "ownerId"> {
8
_id?: IOid;
9
date: IMongoDate;
10
startDate?: IMongoDate;
11
endDate?: IMongoDate;
12
messageId: IOid;
13
}
14
15
export interface IMessageDatabase {
16
ownerId: Types.ObjectId;
17
date: Date;
18
_id: Types.ObjectId;
19
sndr: string;
20
msg: string;
21
sub: string;
22
icon: string;
23
highPriority?: boolean;
24
lowPrioNewPlayers?: boolean;
25
startDate?: Date;
26
endDate?: Date;
27
r?: boolean;
28
att?: string[];
29
countedAtt?: ITypeCount[];
30
transmission?: string;
31
arg?: Arg[];
32
}
33
34
export interface Arg {
35
Key: string;
36
Tag: string;
37
}
38
39
//types are wrong
40
// export interface IMessageDatabase {
41
// _id: Types.ObjectId;
42
// messageId: string;
43
// sub: string;
44
// sndr: string;
45
// msg: string;
46
// startDate: Date;
47
// endDate: Date;
48
// date: Date;
49
// contextInfo: string;
50
// icon: string;
51
// att: string[];
52
// modPacks: string[];
53
// countedAtt: string[];
54
// attSpecial: string[];
55
// transmission: string;
56
// ordisReactionTransmission: string;
57
// arg: string[];
58
// r: string;
59
// acceptAction: string;
60
// declineAction: string;
61
// highPriority: boolean;
62
// lowPrioNewPlayers: boolean
63
// gifts: string[];
64
// teleportLoc: string;
65
// RegularCredits: string;
66
// PremiumCredits: string;
67
// PrimeTokens: string;
68
// Coupons: string[];
69
// syndicateAttachment: string[];
70
// tutorialTag: string;
71
// url: string;
72
// urlButtonText: string;
73
// cinematic: string;
74
// requiredLevel: string;
75
// }
76
const messageSchema = new Schema<IMessageDatabase>(
77
{
78
ownerId: Schema.Types.ObjectId,
79
sndr: String,
80
msg: String,
81
sub: String,
82
icon: String,
83
highPriority: Boolean,
84
lowPrioNewPlayers: Boolean,
85
startDate: Date,
86
endDate: Date,
87
r: Boolean,
88
att: { type: [String], default: undefined },
89
countedAtt: { type: [typeCountSchema], default: undefined },
90
transmission: String,
91
arg: {
92
type: [
93
{
94
Key: String,
95
Tag: String,
96
_id: false
97
}
98
],
99
default: undefined
100
}
101
},
102
{ timestamps: { createdAt: "date", updatedAt: false }, id: false }
103
);
104
105
messageSchema.virtual("messageId").get(function (this: IMessageDatabase) {
106
return toOid(this._id);
107
});
108
109
messageSchema.set("toJSON", {
110
virtuals: true,
111
transform(_document, returnedObject) {
112
delete returnedObject.ownerId;
113
114
const messageDatabase = returnedObject as IMessageDatabase;
115
const messageClient = returnedObject as IMessageClient;
116
117
delete returnedObject._id;
118
delete returnedObject.__v;
119
120
messageClient.date = toMongoDate(messageDatabase.date);
121
122
if (messageDatabase.startDate && messageDatabase.endDate) {
123
messageClient.startDate = toMongoDate(messageDatabase.startDate);
124
125
messageClient.endDate = toMongoDate(messageDatabase.endDate);
126
}
127
}
128
});
129
130
export const Inbox = model<IMessageDatabase>("Inbox", messageSchema, "inbox");
@@ -1,4 +1,4 @@
1
import { Document, Model, Schema, Types, model } from "mongoose";
1
import { Document, HydratedDocument, Model, Schema, Types, model } from "mongoose";
2
2
import {
3
3
IFlavourItem,
4
4
IRawUpgrade,
@@ -7,7 +7,7 @@ import {
7
7
IBooster,
8
8
IInventoryClient,
9
9
ISlots,
10
IMailbox,
10
IMailboxDatabase,
11
11
IDuviriInfo,
12
12
IPendingRecipe as IPendingRecipeDatabase,
13
13
IPendingRecipeResponse,
@@ -53,6 +53,7 @@ import {
53
53
IUpgradeDatabase,
54
54
ICrewShipMemberDatabase,
55
55
ICrewShipMemberClient,
56
IMailboxClient,
56
57
TEquipmentKey,
57
58
equipmentKeys,
58
59
IKubrowPetDetailsDatabase,
@@ -298,22 +299,18 @@ FlavourItemSchema.set("toJSON", {
298
299
}
299
300
});
300
301
301
// "Mailbox": { "LastInboxId": { "$oid": "123456780000000000000000" } }
302
const MailboxSchema = new Schema<IMailbox>(
302
const MailboxSchema = new Schema<IMailboxDatabase>(
303
303
{
304
LastInboxId: {
305
type: Schema.Types.ObjectId,
306
set: (v: IMailbox["LastInboxId"]): string => v.$oid.toString()
307
}
304
LastInboxId: Schema.Types.ObjectId
308
305
},
309
306
{ id: false, _id: false }
310
307
);
311
308
312
309
MailboxSchema.set("toJSON", {
313
310
transform(_document, returnedObject) {
314
delete returnedObject.__v;
315
//TODO: there is a lot of any here
316
returnedObject.LastInboxId = toOid(returnedObject.LastInboxId as Types.ObjectId);
311
const mailboxDatabase = returnedObject as HydratedDocument<IMailboxDatabase, { __v?: number }>;
312
delete mailboxDatabase.__v;
313
(returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId);
317
314
}
318
315
});
319
316
@@ -6,20 +6,6 @@ const opts = {
6
6
toObject: { virtuals: true }
7
7
} satisfies SchemaOptions;
8
8
9
// {
10
// toJSON: { virtuals: true }
11
// }
12
// {
13
// virtuals: {
14
// id: {
15
// get() {
16
// return "test";
17
// }
18
// },
19
// toJSON: { virtuals: true }
20
// }
21
// }
22
23
9
const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
24
10
{
25
11
email: { type: String, required: true, unique: true },
@@ -34,14 +20,14 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
34
20
ConsentNeeded: { type: Boolean, required: true },
35
21
TrackedSettings: { type: [String], default: [] },
36
22
Nonce: { type: Number, default: 0 },
37
LastLoginDay: { type: Number }
23
LastLoginDay: { type: Number },
24
LatestEventMessageDate: { type: Date, required: true }
38
25
},
39
26
opts
40
27
);
41
28
42
29
databaseAccountSchema.set("toJSON", {
43
30
transform(_document, returnedObject) {
44
//returnedObject.id = returnedObject._id.toString();
45
31
delete returnedObject._id;
46
32
delete returnedObject.__v;
47
33
},
@@ -14,6 +14,7 @@ import { importController } from "@/src/controllers/custom/importController";
14
14
15
15
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
16
16
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
17
import { createMessageController } from "@/src/controllers/custom/createMessageController";
17
18
18
19
const customRouter = express.Router();
19
20
@@ -25,6 +26,7 @@ customRouter.get("/deleteAccount", deleteAccountController);
25
26
customRouter.get("/renameAccount", renameAccountController);
26
27
27
28
customRouter.post("/createAccount", createAccountController);
29
customRouter.post("/createMessage", createMessageController);
28
30
customRouter.post("/addItems", addItemsController);
29
31
customRouter.post("/addXp", addXpController);
30
32
customRouter.post("/import", importController);
@@ -0,0 +1,66 @@
1
import { IMessageDatabase, Inbox } from "@/src/models/inboxModel";
2
import { getAccountForRequest } from "@/src/services/loginService";
3
import { HydratedDocument } from "mongoose";
4
import { Request } from "express";
5
import messages from "@/static/fixed_responses/messages.json";
6
import { logger } from "@/src/utils/logger";
7
8
export const getAllMessagesSorted = async (accountId: string): Promise<HydratedDocument<IMessageDatabase>[]> => {
9
const inbox = await Inbox.find({ ownerId: accountId }).sort({ date: -1 });
10
return inbox;
11
};
12
13
export const getMessage = async (messageId: string): Promise<HydratedDocument<IMessageDatabase>> => {
14
const message = await Inbox.findOne({ _id: messageId });
15
16
if (!message) {
17
throw new Error(`Message not found ${messageId}`);
18
}
19
return message;
20
};
21
22
export const deleteMessageRead = async (messageId: string): Promise<void> => {
23
await Inbox.findOneAndDelete({ _id: messageId, r: true });
24
};
25
26
export const deleteAllMessagesRead = async (accountId: string): Promise<void> => {
27
await Inbox.deleteMany({ ownerId: accountId, r: true });
28
};
29
30
export const createNewEventMessages = async (req: Request) => {
31
const account = await getAccountForRequest(req);
32
const latestEventMessageDate = account.LatestEventMessageDate;
33
34
//TODO: is baroo there? create these kind of messages too (periodical messages)
35
const newEventMessages = messages.Messages.filter(m => new Date(m.eventMessageDate) > latestEventMessageDate);
36
37
if (newEventMessages.length === 0) {
38
logger.debug(`No new event messages. Latest event message date: ${latestEventMessageDate.toISOString()}`);
39
return;
40
}
41
42
const savedEventMessages = await createMessage(account._id.toString(), newEventMessages);
43
logger.debug("created event messages", savedEventMessages);
44
45
const latestEventMessage = newEventMessages.reduce((prev, current) =>
46
prev.eventMessageDate > current.eventMessageDate ? prev : current
47
);
48
49
console.log("latestEventMessage", latestEventMessage);
50
account.LatestEventMessageDate = new Date(latestEventMessage.eventMessageDate);
51
await account.save();
52
};
53
54
export const createMessage = async (accountId: string, messages: IMessageCreationTemplate[]) => {
55
const ownerIdMessages = messages.map(m => ({
56
...m,
57
ownerId: accountId
58
}));
59
60
const savedMessages = await Inbox.insertMany(ownerIdMessages);
61
return savedMessages;
62
};
63
64
export interface IMessageCreationTemplate extends Omit<IMessageDatabase, "_id" | "date" | "ownerId"> {
65
ownerId?: string;
66
}
@@ -474,6 +474,18 @@ export const addItem = async (
474
474
throw new Error(errorMessage);
475
475
};
476
476
477
export const addItems = async (
478
inventory: TInventoryDatabaseDocument,
479
items: ITypeCount[],
480
inventoryChanges: IInventoryChanges = {}
481
): Promise<IInventoryChanges> => {
482
for (const item of items) {
483
const inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount);
484
combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges);
485
}
486
return inventoryChanges;
487
};
488
477
489
//TODO: maybe genericMethod for all the add methods, they share a lot of logic
478
490
export const addSentinel = (
479
491
inventory: TInventoryDatabaseDocument,
@@ -37,10 +37,10 @@ export interface IInventoryDatabase
37
37
> {
38
38
accountOwnerId: Types.ObjectId;
39
39
Created: Date;
40
TrainingDate: Date; // TrainingDate changed from IMongoDate to Date
40
TrainingDate: Date;
41
41
LoadOutPresets: Types.ObjectId; // LoadOutPresets changed from ILoadOutPresets to Types.ObjectId for population
42
Mailbox: Types.ObjectId; // Mailbox changed from IMailbox to Types.ObjectId
43
GuildId?: Types.ObjectId; // GuildId changed from ?IOid to ?Types.ObjectId
42
Mailbox?: IMailboxDatabase;
43
GuildId?: Types.ObjectId;
44
44
PendingRecipes: IPendingRecipe[];
45
45
QuestKeys: IQuestKeyDatabase[];
46
46
BlessingCooldown: Date;
@@ -127,10 +127,14 @@ export interface IDuviriInfo {
127
127
NumCompletions: number;
128
128
}
129
129
130
export interface IMailbox {
130
export interface IMailboxClient {
131
131
LastInboxId: IOid;
132
132
}
133
133
134
export interface IMailboxDatabase {
135
LastInboxId: Types.ObjectId;
136
}
137
134
138
export type TSolarMapRegion =
135
139
| "Earth"
136
140
| "Ceres"
@@ -202,7 +206,7 @@ export interface IInventoryClient extends IDailyAffiliations {
202
206
KahlLoadOuts: IOperatorConfigClient[];
203
207
204
208
DuviriInfo: IDuviriInfo;
205
Mailbox: IMailbox;
209
Mailbox?: IMailboxClient;
206
210
SubscribedToEmails: number;
207
211
Created: IMongoDate;
208
212
RewardSeed: number;
@@ -238,7 +242,7 @@ export interface IInventoryClient extends IDailyAffiliations {
238
242
ActiveQuest: string;
239
243
FlavourItems: IFlavourItem[];
240
244
LoadOutPresets: ILoadOutPresets;
241
CurrentLoadOutIds: IOid[]; // we store it in the database using this representation as well :/
245
CurrentLoadOutIds: IOid[]; //TODO: we store it in the database using this representation as well :/
242
246
Missions: IMission[];
243
247
RandomUpgradesIdentified?: number;
244
248
LastRegionPlayed: TSolarMapRegion;