Further login process
* APIUtils addition: query validation * Coach and Server accounts are now properly created if they do not exist * Profiles now cannot be IDs 1 or 2 (reservedIds) * Fixed profile username exists bug * Added relationship manager * Started relationship management * DeviceClass and VRMovementMode enum defaults for reserved profiles * Presence update simplification * Progression fixes * Relationship query and object fixes * Base configuration is now rate limited * Progression route no longer requires authentication, instead is rate limited * Base relationships with reserved profiles (Coach and Server) * DeviceClass required for login * Get presence route * Socket route no longer logs * Socket target base finished
This commit is contained in:
@@ -74,6 +74,15 @@ export const validateRequestBody = <T>(schema: z.ZodSchema<T>) => (rq: express.R
|
||||
rs.status(400).json(genericResponseFormat(true, "Bad request", undefined, error.errors));
|
||||
}
|
||||
};
|
||||
export const validateQuery = <T>(schema: z.ZodSchema<T>) => (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
try {
|
||||
schema.parse(rq.query);
|
||||
nxt();
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError)
|
||||
rs.status(400).json(genericResponseFormat(true, "Bad request", undefined, error.errors));
|
||||
}
|
||||
};
|
||||
|
||||
type genericResponse = {
|
||||
failure: boolean,
|
||||
|
||||
@@ -6,7 +6,7 @@ import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const log = new Logging("Presence");
|
||||
|
||||
interface PresenceExport {
|
||||
export interface PresenceExport {
|
||||
roomInstance: RoomInstance | null;
|
||||
playerId: number;
|
||||
statusVisibility: PlayerStatusVisibility;
|
||||
@@ -14,7 +14,6 @@ interface PresenceExport {
|
||||
vrMovementMode?: VRMovementMode;
|
||||
}
|
||||
|
||||
// Hot mess
|
||||
class PlayerPresence {
|
||||
|
||||
intervalId: number;
|
||||
@@ -59,27 +58,21 @@ class PlayerPresence {
|
||||
if (visibilityResult.success) this.statusVisibility = visibilityResult.data;
|
||||
}
|
||||
|
||||
async updateEnums() {
|
||||
async update() {
|
||||
this.updateOffline();
|
||||
if (!this.offline) await this.updateStatusVisibility();
|
||||
else this.statusVisibility = PlayerStatusVisibility.Offline;
|
||||
|
||||
// deviceClass
|
||||
const DeviceClassEnum = z.nativeEnum(DeviceClass);
|
||||
type DeviceClassEnum = z.infer<typeof DeviceClassEnum>;
|
||||
this.deviceClass = await this.#profile.getKnownDeviceClass();
|
||||
|
||||
const deviceClassResult = DeviceClassEnum.safeParse(await this.#profile.getKnownDeviceClass());
|
||||
if (deviceClassResult.success) this.deviceClass = deviceClassResult.data;
|
||||
|
||||
// vrMovementMode
|
||||
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
||||
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>;
|
||||
|
||||
const vrMovementMoveResult = VRMovementModeEnum.safeParse(await this.#profile.getVRMovementMode());
|
||||
if (vrMovementMoveResult.success) this.vrMovementMode = vrMovementMoveResult.data;
|
||||
this.vrMovementMode = await this.#profile.getVRMovementMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export presence object. Please make sure to update values with `Presence.update()` (async) before calling this.
|
||||
*/
|
||||
async export() {
|
||||
await this.updateEnums();
|
||||
await this.update();
|
||||
const exp: PresenceExport = {
|
||||
playerId: this.playerId,
|
||||
roomInstance: this.roomInstance,
|
||||
@@ -108,7 +101,10 @@ class PresenceBase {
|
||||
*/
|
||||
async getAllPresences() {
|
||||
const presSet: Set<PresenceExport> = new Set();
|
||||
for (const pres of presence.values()) presSet.add(await pres.export());
|
||||
for (const pres of presence.values()) {
|
||||
await pres.update();
|
||||
presSet.add(await pres.export());
|
||||
}
|
||||
return presSet;
|
||||
}
|
||||
|
||||
@@ -119,7 +115,7 @@ class PresenceBase {
|
||||
async create(player: Profile) {
|
||||
if (!presence.values().find(pres => pres.playerId == player.getId())) {
|
||||
const pres = new PlayerPresence(player);
|
||||
await pres.updateEnums();
|
||||
await pres.update();
|
||||
presence.add(pres);
|
||||
}
|
||||
log.d(`Presences: ${JSON.stringify(Array.from(await Presence.getAllPresences()))}`);
|
||||
@@ -130,7 +126,7 @@ class PresenceBase {
|
||||
if (pres) return pres;
|
||||
else {
|
||||
const pres = new PlayerPresence(player, true);
|
||||
await pres.updateEnums();
|
||||
await pres.update();
|
||||
presence.add(pres);
|
||||
return pres;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
||||
if (xp >= item.RequiredXp) {
|
||||
|
||||
const current = config?.LevelProgressionMaps[config?.LevelProgressionMaps.indexOf(item)];
|
||||
if (typeof current == 'undefined') return null;
|
||||
if (typeof current == 'undefined') return 1;
|
||||
else return current.Level;
|
||||
|
||||
}
|
||||
@@ -72,7 +72,9 @@ export class ProfileProgressionManager extends ProfileContentManager {
|
||||
const parsedData = parseInt(data);
|
||||
if (isNaN(parsedData)) {
|
||||
log.w(`Parsed xp data for ${this.profileId} is NaN!`);
|
||||
return 0; // fallback since progression data is required
|
||||
const one = config?.LevelProgressionMaps[1];
|
||||
if (typeof one == 'undefined' && !one) return 0; // fallback since progression data is required
|
||||
else return one.RequiredXp;
|
||||
} else return parsedData;
|
||||
}
|
||||
|
||||
|
||||
64
src/data/profile/relationships.ts
Normal file
64
src/data/profile/relationships.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Profile } from "../profiles.ts";
|
||||
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||
|
||||
enum RelationshipType {
|
||||
None,
|
||||
FriendRequestSent,
|
||||
FriendRequestReceived,
|
||||
Friend
|
||||
}
|
||||
|
||||
enum ReciprocalStatus {
|
||||
None,
|
||||
Local,
|
||||
Remote,
|
||||
Mutual
|
||||
}
|
||||
|
||||
interface Relationship {
|
||||
PlayerID: number, // target player, not this player
|
||||
RelationshipType: RelationshipType,
|
||||
Muted: ReciprocalStatus,
|
||||
Ignored: ReciprocalStatus
|
||||
}
|
||||
|
||||
export class ProfileRelationshipManager extends ProfileContentManager {
|
||||
|
||||
#baseRelationships: Relationship[] = [
|
||||
{
|
||||
PlayerID: 1,
|
||||
RelationshipType: RelationshipType.Friend,
|
||||
Muted: ReciprocalStatus.None,
|
||||
Ignored: ReciprocalStatus.None
|
||||
},
|
||||
{
|
||||
PlayerID: 2,
|
||||
RelationshipType: RelationshipType.Friend,
|
||||
Muted: ReciprocalStatus.None,
|
||||
Ignored: ReciprocalStatus.None
|
||||
}
|
||||
]
|
||||
|
||||
getRelationships() {
|
||||
return this.#baseRelationships; // temporary
|
||||
}
|
||||
|
||||
setPlayerIgnored(player: Profile) {
|
||||
|
||||
}
|
||||
|
||||
setPlayerMuted(player: Profile) {
|
||||
|
||||
}
|
||||
|
||||
getPlayerRelationship(player: Profile) {
|
||||
|
||||
}
|
||||
|
||||
sendPlayerFriendRequest(player: Profile) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -5,9 +5,10 @@ export class ProfileReputationManager extends ProfileContentManager {
|
||||
async getReputation() { // async temporary
|
||||
return {
|
||||
AccountId: this.profileId,
|
||||
Noterity: 0.0,
|
||||
Noteriety: 0.0,
|
||||
CheerGeneral: 0,
|
||||
CheerHelpful: 0,
|
||||
CheerGreatHost: 0,
|
||||
CheerSportsman: 0,
|
||||
CheerCreative: 0,
|
||||
CheerCredit: 0,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ProfileSettingsManager } from "./profile/settings.ts";
|
||||
import { ProfileProgressionManager } from "./profile/progression.ts";
|
||||
import { ProfileReputationManager } from "./profile/reputation.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { ProfileRelationshipManager } from "./profile/relationships.ts";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
@@ -19,11 +20,17 @@ const log = new Logging("Profiles");
|
||||
|
||||
interface ProfileInitOptions {
|
||||
username: string;
|
||||
/**
|
||||
* Ignore the random generation of profile IDs; use this ID.
|
||||
*
|
||||
* Make sure the target account does not exist before using this option.
|
||||
*/
|
||||
id?: number;
|
||||
}
|
||||
interface AccountExport {
|
||||
accountId: number;
|
||||
profileImage: string;
|
||||
isJunior: boolean;
|
||||
isJunior?: boolean;
|
||||
platforms: number;
|
||||
username: string;
|
||||
displayName: string;
|
||||
@@ -34,6 +41,8 @@ export interface ProfileTokenFormat extends TokenBaseFormat {
|
||||
typ: AuthType.Game;
|
||||
}
|
||||
|
||||
const reservedIds = [1, 2];
|
||||
|
||||
class Profile {
|
||||
static async exists(id: number) {
|
||||
return (await Redis.Database.exists(
|
||||
@@ -44,6 +53,11 @@ class Profile {
|
||||
),
|
||||
)) >= 1;
|
||||
}
|
||||
static async existsByName(name: string) {
|
||||
return (await Redis.Database.exists(
|
||||
Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name),
|
||||
)) >= 1;
|
||||
}
|
||||
|
||||
static async getUniqueId() {
|
||||
let id = Math.round(Math.random() * Math.pow(2, 31));
|
||||
@@ -55,18 +69,15 @@ class Profile {
|
||||
Redis.KeyGroups.Profiles.Username,
|
||||
),
|
||||
)) >= 1
|
||||
) {
|
||||
id = await this.getUniqueId();
|
||||
}
|
||||
|| reservedIds.includes(id)
|
||||
) id = await this.getUniqueId();
|
||||
return id;
|
||||
}
|
||||
|
||||
static async byName(name: string) {
|
||||
const id = await Redis.Database.get(
|
||||
Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name),
|
||||
);
|
||||
const id = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profile_Usernames, name));
|
||||
if (id == null) return null;
|
||||
else return new Profile(parseInt(id, 10));
|
||||
else return new Profile(parseInt(id));
|
||||
}
|
||||
|
||||
static async getUniqueUsername() {
|
||||
@@ -83,12 +94,11 @@ class Profile {
|
||||
static async init(options?: ProfileInitOptions) {
|
||||
const optionsSpecified = typeof options !== "undefined";
|
||||
|
||||
if (options?.username) {
|
||||
const existingUser = await Profile.byName(options.username);
|
||||
if (existingUser == null) return null;
|
||||
}
|
||||
if (options?.username && await Profile.existsByName(options.username)) return null;
|
||||
|
||||
const newId = await this.getUniqueId();
|
||||
let newId: number;
|
||||
if (options?.id) newId = options.id;
|
||||
else newId = await this.getUniqueId();
|
||||
const newUsername = optionsSpecified
|
||||
? options.username
|
||||
: await this.getUniqueUsername();
|
||||
@@ -153,6 +163,7 @@ class Profile {
|
||||
Settings = new ProfileSettingsManager();
|
||||
Progression = new ProfileProgressionManager();
|
||||
Reputation = new ProfileReputationManager();
|
||||
Relationships = new ProfileRelationshipManager();
|
||||
|
||||
constructor(id: number) {
|
||||
this.#id = id;
|
||||
@@ -161,6 +172,7 @@ class Profile {
|
||||
this.Settings.setProfile(this.#id);
|
||||
this.Progression.setProfile(this.#id);
|
||||
this.Reputation.setProfile(this.#id);
|
||||
this.Relationships.setProfile(this.#id);
|
||||
}
|
||||
|
||||
setInstance(instance: RoomInstance | null) {
|
||||
@@ -183,11 +195,13 @@ class Profile {
|
||||
return await Profile.getExportAccount(this.#id);
|
||||
}
|
||||
|
||||
async setKnownDeviceClass(deviceClass: string | number) {
|
||||
async setKnownDeviceClass(deviceClass: DeviceClass) {
|
||||
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass), deviceClass);
|
||||
}
|
||||
|
||||
async getKnownDeviceClass() {
|
||||
if (reservedIds.includes(this.#id)) return DeviceClass.Unknown;
|
||||
|
||||
const data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass));
|
||||
if (data == null) {
|
||||
log.w(`No known device class for ${this.#id}`);
|
||||
@@ -212,14 +226,16 @@ class Profile {
|
||||
}
|
||||
|
||||
async getVRMovementMode() {
|
||||
if (reservedIds.includes(this.#id)) return VRMovementMode.Teleport;
|
||||
|
||||
const data = await this.Settings.getSetting(SettingKey.VRMovementMode);
|
||||
if (data == null) {
|
||||
log.w(`No known device class for ${this.#id}`);
|
||||
log.w(`No known VR movement mode for ${this.#id} (harmless if OOBE not ran)`);
|
||||
return VRMovementMode.Teleport;
|
||||
}
|
||||
const parsedData = parseInt(data);
|
||||
if (isNaN(parsedData)) {
|
||||
log.w(`Malformed device class for ${this.#id}`);
|
||||
log.w(`Malformed VR movement mode for ${this.#id}`);
|
||||
return VRMovementMode.Teleport;
|
||||
}
|
||||
|
||||
@@ -258,6 +274,7 @@ class Profile {
|
||||
|
||||
const profiles: Map<number, Profile> = new Map()
|
||||
|
||||
// Control what is available to references
|
||||
class UnifiedProfileBase {
|
||||
|
||||
get(id: number) {
|
||||
@@ -278,6 +295,10 @@ class UnifiedProfileBase {
|
||||
return await Profile.exists(id);
|
||||
}
|
||||
|
||||
async existsByName(name: string) {
|
||||
return await Profile.existsByName(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const UnifiedProfile = new UnifiedProfileBase();
|
||||
|
||||
@@ -188,8 +188,8 @@ try {
|
||||
PLACE TEST HERE
|
||||
*/
|
||||
|
||||
if (!(await UnifiedProfile.exists(1))) UnifiedProfile.create({ username: "Coach" }); // create Coach if they do not exist
|
||||
if (!(await UnifiedProfile.exists(2))) UnifiedProfile.create({ username: "Server" }); // create Server if they do not exist
|
||||
if (!(await UnifiedProfile.existsByName("Coach"))) UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist
|
||||
if (!(await UnifiedProfile.existsByName("Server"))) UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ route.router.post("/create",
|
||||
|
||||
async (_rq, rs) => {
|
||||
const newAcc = await Profile.init();
|
||||
if (newAcc == null) {
|
||||
rs.json({
|
||||
success: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
rs.locals.user.addAssociatedProfile(newAcc.getId());
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import { GameConfigs } from "../../data/config.ts";
|
||||
|
||||
export const route = APIUtils.createRouter("/config");
|
||||
|
||||
route.router.get("/v2", (_rq, rs) => {
|
||||
const rateLimit = new APIUtils.RateLimiter(60, 2);
|
||||
|
||||
route.router.get("/v2", rateLimit.middle(), (_rq, rs) => {
|
||||
const config = GameConfigs.getConfig();
|
||||
if (config == null) rs.sendStatus(500);
|
||||
else rs.json(config);
|
||||
@@ -11,6 +13,7 @@ route.router.get("/v2", (_rq, rs) => {
|
||||
|
||||
route.router.get('/v1/amplitude',
|
||||
APIUtils.setCacheAllowed,
|
||||
rateLimit.middle(),
|
||||
(_rq, rs) => {
|
||||
rs.json({AmplitudeKey: ""});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ route.router.get('/v1/:id',
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
(rq: express.Request<{ id: string }>, rs) => {
|
||||
async (rq: express.Request<{ id: string }>, rs) => {
|
||||
const unparsedPlayerId = rq.params.id;
|
||||
const parsedPlayerId = parseInt(unparsedPlayerId);
|
||||
if (isNaN(parsedPlayerId)) {
|
||||
@@ -18,7 +18,7 @@ route.router.get('/v1/:id',
|
||||
return;
|
||||
}
|
||||
|
||||
rs.json(UnifiedProfile.get(parsedPlayerId).Reputation.getReputation());
|
||||
rs.json(await UnifiedProfile.get(parsedPlayerId).Reputation.getReputation());
|
||||
}
|
||||
|
||||
);
|
||||
@@ -5,11 +5,13 @@ import UnifiedProfile from "../../data/profiles.ts";
|
||||
|
||||
const log = new Logging("ProgressionRoute");
|
||||
|
||||
const rateLimit = new APIUtils.RateLimiter(60, 2);
|
||||
|
||||
export const route = APIUtils.createRouter("/players");
|
||||
|
||||
route.router.get('/v1/progression/:id',
|
||||
|
||||
APIUtils.Authentication,
|
||||
rateLimit.middle(),
|
||||
|
||||
async (rq: express.Request<{ id: string }>, rs) => {
|
||||
const unparsedPlayerId = rq.params.id;
|
||||
|
||||
@@ -9,7 +9,7 @@ route.router.get('/v2/get',
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
|
||||
(_rq, rs) => {
|
||||
rs.json([]); // temporary
|
||||
rs.json(rs.locals.profile.Relationships.getRelationships());
|
||||
}
|
||||
|
||||
);
|
||||
@@ -98,6 +98,7 @@ route.router.post("/token",
|
||||
rq.body.platform === "0",
|
||||
rq.body.ver === '20191120',
|
||||
rq.body.device_class.length === 1,
|
||||
!isNaN(Number(rq.body.device_class)),
|
||||
!(rq.body.device_id.length > 96),
|
||||
!(rq.body.client_secret.length > 96),
|
||||
!(rq.body.platform_id.length > 32),
|
||||
@@ -110,9 +111,8 @@ route.router.post("/token",
|
||||
return;
|
||||
}
|
||||
|
||||
const accounts = await rs.locals.user.getAssociatedProfiles();
|
||||
let targetAccount: number;
|
||||
|
||||
|
||||
if (rq.body.grant_type == 'cached_login') targetAccount = parseInt(rq.body.account_id);
|
||||
else {
|
||||
const refreshToken = rq.body.refresh_token;
|
||||
@@ -128,15 +128,17 @@ route.router.post("/token",
|
||||
requestFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (isNaN(targetAccount)) {
|
||||
requestFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
const accounts = await rs.locals.user.getAssociatedProfiles();
|
||||
if (!accounts.has(targetAccount)) {
|
||||
requestFailed("access_denied");
|
||||
return;
|
||||
@@ -160,6 +162,6 @@ route.router.post("/token",
|
||||
refresh_token: token,
|
||||
});
|
||||
|
||||
await profile.setKnownDeviceClass(rq.body.device_class);
|
||||
await profile.setKnownDeviceClass(Number(rq.body.device_class));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,9 +2,10 @@ import { z } from "zod";
|
||||
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||
import express from "express";
|
||||
import Matchmaking from "../../data/live/base.ts";
|
||||
import Presence from "../../data/live/presence.ts";
|
||||
import Presence, { PresenceExport } from "../../data/live/presence.ts";
|
||||
import { AuthType } from "../../data/users.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import UnifiedProfile from "../../data/profiles.ts";
|
||||
|
||||
const log = new Logging("MatchPlayerRoute");
|
||||
|
||||
@@ -18,6 +19,31 @@ const LoginSchema = z.object({
|
||||
LoginLock: z.string().uuid("LoginLock must be a UUIDv4")
|
||||
});
|
||||
|
||||
route.router.get('/',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
APIUtils.validateQuery(z.object({ id: z.union([z.string(), z.array(z.string())]) })),
|
||||
|
||||
async (rq: express.Request<NoBody, PresenceExport[], NoBody, { id: string[] | string }>, rs) => {
|
||||
let ids: number[] = [];
|
||||
if (typeof rq.query.id == 'object') ids = rq.query.id.map(val => parseInt(val));
|
||||
else ids.push(parseInt(rq.query.id));
|
||||
ids = ids.filter(val => !isNaN(val));
|
||||
|
||||
const presExport: PresenceExport[] = [];
|
||||
for (const id of ids) {
|
||||
const pres = await Presence.get(UnifiedProfile.get(id));
|
||||
await pres.update();
|
||||
presExport.push(await pres.export());
|
||||
}
|
||||
|
||||
rs.json(presExport);
|
||||
log.d(JSON.stringify(presExport));
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
route.router.post('/login',
|
||||
|
||||
APIUtils.Authentication,
|
||||
@@ -40,8 +66,9 @@ route.router.post('/logout',
|
||||
express.urlencoded({extended: true}),
|
||||
APIUtils.validateRequestBody(LoginSchema),
|
||||
|
||||
(rq, rs) => {
|
||||
(_rq, rs) => {
|
||||
Matchmaking.deleteLoginLock(rs.locals.profile);
|
||||
rs.sendStatus(200);
|
||||
}
|
||||
|
||||
)
|
||||
@@ -56,6 +83,7 @@ route.router.post('/heartbeat',
|
||||
|
||||
async (_rq, rs) => {
|
||||
const pres = await Presence.get(rs.locals.profile);
|
||||
await pres.update();
|
||||
log.d(`pres heartbeat for ${rs.locals.profile.getId()}: ${JSON.stringify(await pres.export())}`);
|
||||
rs.json(await pres.export());
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { APIUtils } from "../apiutils.ts";
|
||||
import { Config } from "../config.ts";
|
||||
import { AuthType } from "../data/users.ts";
|
||||
import { SocketHandoff } from "./handoff.ts";
|
||||
import express from "express";
|
||||
|
||||
const config = Config.getConfig();
|
||||
|
||||
@@ -12,8 +11,6 @@ route.router.post('/hub/v1/negotiate',
|
||||
|
||||
APIUtils.Authentication,
|
||||
APIUtils.AuthenticationType(AuthType.Game),
|
||||
express.urlencoded({ extended: true }),
|
||||
APIUtils.logBody,
|
||||
|
||||
(_rq, rs) => {
|
||||
const handoff = new SocketHandoff();
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { Profile } from "../data/profiles.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { Message, MessageKind, SignalMessageType, SignalRMessage, SignalRMessageSchema, TargetResult, TargetResultFailure, TargetResultSuccess, TargetResultType } from "./types.ts";
|
||||
import {
|
||||
CompletionMessage,
|
||||
Message,
|
||||
MessageKind,
|
||||
SignalMessageType,
|
||||
SignalRMessage,
|
||||
SignalRMessageSchema,
|
||||
TargetResult,
|
||||
TargetResultFailure,
|
||||
TargetResultNotATarget,
|
||||
TargetResultSuccess,
|
||||
TargetResultType
|
||||
} from "./types.ts";
|
||||
import { SocketTarget } from "./targets/targetbase.ts";
|
||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||
|
||||
@@ -26,22 +38,55 @@ export class SignalRSocketHandler {
|
||||
|
||||
}
|
||||
|
||||
async #dispatchTarget<T = unknown>(target: string, args: object[]): Promise<TargetResult> {
|
||||
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
|
||||
const targetExec = this.#Targets.get(target);
|
||||
if (!targetExec) return { type: TargetResultType.Failure } as TargetResultFailure;
|
||||
else return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
|
||||
if (!targetExec) return { type: TargetResultType.NotATarget } as TargetResultNotATarget;
|
||||
else {
|
||||
try {
|
||||
return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
|
||||
} catch (err) {
|
||||
this.#log.w(`Target '${target}' function error: ${err}`);
|
||||
if (err instanceof Error) return { type: TargetResultType.Failure, err: err } as TargetResultFailure;
|
||||
else return { type: TargetResultType.Failure, err: `${err}` } as TargetResultFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#onMessage(message: Message) {
|
||||
async #onMessage(message: Message) {
|
||||
if (message.kind == MessageKind.Protocol) {
|
||||
this.sendRaw({});
|
||||
return;
|
||||
} else {
|
||||
this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
|
||||
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
|
||||
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
|
||||
if (res.type == TargetResultType.Success) {
|
||||
const signalRes: CompletionMessage = {
|
||||
type: SignalMessageType.Completion,
|
||||
invocationId: message.data.invocationId,
|
||||
result: JSON.stringify(res.data)
|
||||
}
|
||||
this.sendRaw(signalRes);
|
||||
} else if (res.type == TargetResultType.Failure) {
|
||||
const signalRes: CompletionMessage = {
|
||||
type: SignalMessageType.Completion,
|
||||
invocationId: message.data.invocationId,
|
||||
error: res.err instanceof Error ? res.err.message : res.err
|
||||
}
|
||||
this.sendRaw(signalRes);
|
||||
} else {
|
||||
const signalRes: CompletionMessage = {
|
||||
type: SignalMessageType.Completion,
|
||||
invocationId: message.data.invocationId,
|
||||
error: "Target not found"
|
||||
}
|
||||
this.sendRaw(signalRes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #init() {
|
||||
#init() {
|
||||
this.#log.source += this.#profile.getId().toString();
|
||||
|
||||
this.#log.i(`Created hub socket`);
|
||||
@@ -82,6 +127,9 @@ export class SignalRSocketHandler {
|
||||
|
||||
sendRaw(data: object) {
|
||||
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
||||
// todo sometime: make this less confusing
|
||||
const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
|
||||
this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { SocketTarget } from "./targetbase.ts";
|
||||
|
||||
const ArgumentSchema = z.object({
|
||||
PlayerIds: z.array(z.number())
|
||||
});
|
||||
|
||||
export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
|
||||
subscriptions: number[] = [];
|
||||
@@ -9,8 +14,12 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
override async exec(_args: (object | string | number | boolean)[]) {
|
||||
return;
|
||||
override async exec(args: unknown) {
|
||||
const parsed = ArgumentSchema.safeParse(args);
|
||||
if (parsed.success) {
|
||||
this.setSubscriptions(parsed.data.PlayerIds);
|
||||
return;
|
||||
} else throw new Error("Invalid arguments");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class SocketTarget {
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
async exec(_args: (object | string | number | boolean)[]) {
|
||||
async exec(_args: unknown) {
|
||||
throw new Error("Execution for this target is not set.");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ export enum MessageKind {
|
||||
Protocol,
|
||||
Data
|
||||
}
|
||||
interface MessageBase {
|
||||
export interface MessageBase {
|
||||
kind: MessageKind
|
||||
}
|
||||
interface DataMessage extends MessageBase {
|
||||
export interface DataMessage extends MessageBase {
|
||||
kind: MessageKind.Data,
|
||||
data: SignalRMessage
|
||||
}
|
||||
interface ProtocolMessage extends MessageBase {
|
||||
export interface ProtocolMessage extends MessageBase {
|
||||
kind: MessageKind.Protocol
|
||||
}
|
||||
export type Message = ProtocolMessage | DataMessage;
|
||||
@@ -34,68 +34,68 @@ export enum SignalMessageType {
|
||||
Close
|
||||
}
|
||||
|
||||
interface BaseMessage {
|
||||
export interface BaseMessage {
|
||||
type: SignalMessageType;
|
||||
}
|
||||
|
||||
interface InvocationMessage extends BaseMessage {
|
||||
export interface InvocationMessage extends BaseMessage {
|
||||
type: SignalMessageType.Invocation;
|
||||
target: string;
|
||||
arguments: unknown[];
|
||||
invocationId?: string;
|
||||
}
|
||||
|
||||
interface StreamItemMessage extends BaseMessage {
|
||||
export interface StreamItemMessage extends BaseMessage {
|
||||
type: SignalMessageType.StreamItem;
|
||||
invocationId: string;
|
||||
item: unknown;
|
||||
}
|
||||
|
||||
interface CompletionMessage extends BaseMessage {
|
||||
export interface CompletionMessage extends BaseMessage {
|
||||
type: SignalMessageType.Completion;
|
||||
invocationId: string;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface PingMessage extends BaseMessage {
|
||||
export interface PingMessage extends BaseMessage {
|
||||
type: SignalMessageType.Ping;
|
||||
}
|
||||
|
||||
interface CloseMessage extends BaseMessage {
|
||||
export interface CloseMessage extends BaseMessage {
|
||||
type: SignalMessageType.Close;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const BaseMessageSchema = z.object({
|
||||
export const BaseMessageSchema = z.object({
|
||||
type: z.nativeEnum(SignalMessageType),
|
||||
});
|
||||
|
||||
const InvocationMessageSchema = BaseMessageSchema.extend({
|
||||
export const InvocationMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Invocation),
|
||||
target: z.string(),
|
||||
arguments: z.array(z.unknown()),
|
||||
invocationId: z.string().optional(),
|
||||
});
|
||||
|
||||
const StreamItemMessageSchema = BaseMessageSchema.extend({
|
||||
export const StreamItemMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.StreamItem),
|
||||
invocationId: z.string(),
|
||||
item: z.unknown(),
|
||||
});
|
||||
|
||||
const CompletionMessageSchema = BaseMessageSchema.extend({
|
||||
export const CompletionMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Completion),
|
||||
invocationId: z.string(),
|
||||
result: z.unknown().optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
const PingMessageSchema = BaseMessageSchema.extend({
|
||||
export const PingMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Ping),
|
||||
});
|
||||
|
||||
const CloseMessageSchema = BaseMessageSchema.extend({
|
||||
export const CloseMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Close),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
@@ -113,7 +113,7 @@ export enum TargetResultType {
|
||||
Failure,
|
||||
NotATarget
|
||||
}
|
||||
interface TargetResultBase {
|
||||
export interface TargetResultBase {
|
||||
type: TargetResultType
|
||||
}
|
||||
export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
|
||||
@@ -122,6 +122,7 @@ export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
|
||||
}
|
||||
export interface TargetResultFailure extends TargetResultBase {
|
||||
type: TargetResultType.Failure
|
||||
err: string | Error
|
||||
}
|
||||
export interface TargetResultNotATarget extends TargetResultBase {
|
||||
type: TargetResultType.NotATarget
|
||||
|
||||
Reference in New Issue
Block a user