XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
返回提交历史

XFEstudio/SpaceNinjaServer

feat: Inbox (#876)

50c280cf
OrdisPrime <134585663+OrdisPrime@users.noreply.github.com>
提交于

代码差异

14 个文件 +367 -70
Modified .eslintrc +3 -1
@@ -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": {
Modified src/controllers/api/inboxController.ts +80 -5
@@ -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 };
Modified src/controllers/api/loginController.ts +3 -2
@@ -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));
Added src/controllers/custom/createMessageController.ts +14 -0
@@ -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 };
Modified src/helpers/customHelpers/customHelpers.ts +2 -1
@@ -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
Added src/models/inboxModel.ts +130 -0
@@ -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");
Modified src/models/inventoryModels/inventoryModel.ts +8 -11
@@ -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
Modified src/models/loginModel.ts +2 -16
@@ -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 },
Modified src/routes/custom.ts +2 -0
@@ -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);
Added src/services/inboxService.ts +66 -0
@@ -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 }
Modified src/services/inventoryService.ts +12 -0
@@ -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,
Modified src/types/inventoryTypes/inventoryTypes.ts +10 -6
@@ -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;
Modified src/types/loginTypes.ts +22 -21
Renamed static/fixed_responses/messages.json +13 -7