forked from zombieb/galvanic-corrosion-rewrite
image library changes, bit flags changes
This commit is contained in:
@@ -11,11 +11,12 @@ import { SignalRSocketHandler } from "./server/socket/signalr/socket.ts";
|
||||
import { PushNotificationId } from "./server/socket/signalr/types.ts";
|
||||
import { genericResponse } from "./util/api.ts";
|
||||
import { getNetConfig } from "./net.ts";
|
||||
import { PlatformType, TokenFormat, TokenType } from "./server/platforms/types.ts";
|
||||
import { PlatformMask, PlatformType, TokenFormat, TokenType } from "./server/platforms/types.ts";
|
||||
import { HonoEnv } from "./util/types.ts";
|
||||
import { Context } from "@hono/hono";
|
||||
import { compress } from "@hono/hono/compress";
|
||||
import { env } from "./env.ts";
|
||||
import { BitFlags } from "./util/flags.ts";
|
||||
|
||||
LoggingConfiguration.resetTimeFormat = TimeFormat.Unix;
|
||||
LoggingConfiguration.resetLogTiming = LogTiming.Microtask;
|
||||
@@ -115,7 +116,7 @@ const server = Deno.serve({
|
||||
|
||||
const profile = await Server.Profiles.get(payload.sub);
|
||||
if (!profile) return new Response("Internal Server Error (profile)", { status: 500 });
|
||||
const { response, socket } = Deno.upgradeWebSocket(req);
|
||||
const { response, socket } = Deno.upgradeWebSocket(req, { idleTimeout: 60 });
|
||||
const handler = new SignalRSocketHandler(socket, profile);
|
||||
gameSockets.add(handler);
|
||||
socket.onclose = () => {
|
||||
@@ -190,6 +191,6 @@ Server.Commands.addRootCommand(new Command({
|
||||
|
||||
Server.on('server.start', () => {
|
||||
Server.Profiles.get(1).then(prof => {
|
||||
if (!prof) Server.Profiles.create(PlatformType.HeadlessBot, "", "Coach", 1);
|
||||
if (!prof) Server.Profiles.create(PlatformType.HeadlessBot, new BitFlags<PlatformMask>(PlatformMask.All), "", "Coach", 1);
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,11 @@ import { RateLimiter, recNetResultResponse, statusResponse } from "../../../util
|
||||
import Server from "../../../server/server.ts";
|
||||
import z from "zod";
|
||||
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
|
||||
import { PlatformType } from "../../../server/platforms/types.ts";
|
||||
import { PlatformMask, PlatformType } from "../../../server/platforms/types.ts";
|
||||
import Steam from "../../../util/steam/steam.ts";
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
import { accountRoute } from "./me/root.ts";
|
||||
import { BitFlags } from "../../../util/flags.ts";
|
||||
|
||||
export const route = createHonoRoute('/account');
|
||||
|
||||
@@ -50,7 +51,7 @@ route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form'
|
||||
const cachedlogins = await Server.Platforms.getCachedLogins(form.platform, form.platformId, true);
|
||||
if (cachedlogins.length == 0) {
|
||||
|
||||
const profile = await Server.Profiles.create(form.platform, form.platformId, steam[0].realname ?? steam[0].personaname);
|
||||
const profile = await Server.Profiles.create(form.platform, new BitFlags<PlatformMask>(PlatformMask.Steam), form.platformId, steam[0].realname ?? steam[0].personaname);
|
||||
if (!profile) return recNetResultResponse(c, HTTPStatus.OK, false, "Account could not be created");
|
||||
|
||||
Server.Content.steamAvatarDownloadForProfile(profile, steam[0].avatarfull);
|
||||
@@ -62,7 +63,7 @@ route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form'
|
||||
|
||||
} else {
|
||||
|
||||
const profile = await Server.Profiles.create(form.platform, form.platformId);
|
||||
const profile = await Server.Profiles.create(form.platform, new BitFlags<PlatformMask>(PlatformMask.Steam), form.platformId);
|
||||
if (!profile) return recNetResultResponse(c, HTTPStatus.OK, false, "Account could not be created");
|
||||
|
||||
return c.json({
|
||||
|
||||
8
src/routes/accounts/routes/parentalcontrol.ts
Normal file
8
src/routes/accounts/routes/parentalcontrol.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { authenticate } from "../../../util/api.ts";
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
|
||||
export const route = createHonoRoute("/parentalcontrol");
|
||||
|
||||
route.app.get('/me', authenticate, c => {
|
||||
return c.json({ accountId: c.get('profile').getId(), disallowInAppPurchases: false });
|
||||
});
|
||||
@@ -20,7 +20,7 @@ route.app.get('/v1/:id', authenticate, typedZValidator('param', getRepIdParamSch
|
||||
const getRepBulkBodySchema = z.object({
|
||||
Ids: z.array(z.coerce.number())
|
||||
});
|
||||
route.app.post('/v1/bulk', authenticate, typedZValidator('form', getRepBulkBodySchema), async c => {
|
||||
route.app.post('/v1/bulk', typedZValidator('form', getRepBulkBodySchema), async c => {
|
||||
const ids = c.req.valid('form').Ids;
|
||||
|
||||
if (typeof ids == 'object') {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createHonoRoute } from "../../../util/import.ts";
|
||||
import { typedZValidator } from "../../../util/validators.ts";
|
||||
import Server from "../../../server/server.ts";
|
||||
import { authenticate } from "../../../util/api.ts";
|
||||
import { RoomAccessibility } from "../../../server/rooms/internal/RoomDataTypes.ts";
|
||||
|
||||
export const route = createHonoRoute("/rooms");
|
||||
|
||||
@@ -35,8 +36,9 @@ route.app.get('/v2/myrooms', authenticate, async c => {
|
||||
return c.json(rooms);
|
||||
});
|
||||
|
||||
route.app.get('/v1/hot', authenticate, async c => {
|
||||
route.app.get('/v1/hot', async c => {
|
||||
// temporary! parse tags and manage "hot" rooms
|
||||
const agRoomIds = Server.Rooms.getAgRoomIds();
|
||||
const factories = await Server.Rooms.getMany(...agRoomIds.values().toArray());
|
||||
return c.json((await Promise.all(factories.map(factory => factory.export()))).map(roomDetails => roomDetails.Room));
|
||||
return c.json((await Promise.all(factories.map(factory => factory.export()))).map(roomDetails => roomDetails.Room).filter(room => room.Accessibility == RoomAccessibility.Public));
|
||||
});
|
||||
@@ -100,7 +100,8 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
||||
if (!profile) return error(TokenRequestError.AccessDenied);
|
||||
|
||||
return c.json({
|
||||
token: await Server.Platforms.getToken(profile, TokenType.Access),
|
||||
access_token: await Server.Platforms.getToken(profile, TokenType.Access),
|
||||
refresh_token: await Server.Platforms.getToken(profile, TokenType.Refresh),
|
||||
});
|
||||
} catch (err) {
|
||||
log.w(`Authentication error (token req): ${(err as Error).stack}`);
|
||||
@@ -112,12 +113,10 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
||||
const profile = await Server.Profiles.get(form.account_id);
|
||||
if (!profile) return error(TokenRequestError.InvalidRequest, "No such profile");
|
||||
await Server.Platforms.updateLastLoginTime(form.platform, form.platform_id, form.account_id);
|
||||
const accessToken = await Server.Platforms.getToken(profile, TokenType.Access);
|
||||
const refreshToken = await Server.Platforms.getToken(profile, TokenType.Refresh);
|
||||
|
||||
return c.json({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
access_token: await Server.Platforms.getToken(profile, TokenType.Access),
|
||||
refresh_token: await Server.Platforms.getToken(profile, TokenType.Refresh),
|
||||
});
|
||||
} else return error(TokenRequestError.InvalidRequest, "No such profile");
|
||||
});
|
||||
@@ -8,28 +8,51 @@ import Logging from "@proxnet/undead-logging";
|
||||
import { statusResponse } from "../../util/api.ts";
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
import { Buffer } from "node:buffer";
|
||||
import sharp from "sharp";
|
||||
import { Image } from "imagescript";
|
||||
|
||||
export const route = createHonoRoute('/img');
|
||||
|
||||
interface StaticImageRedirect {
|
||||
org: string,
|
||||
new: string
|
||||
}
|
||||
|
||||
const redirects: StaticImageRedirect[] = [
|
||||
{
|
||||
org: "PaintballVR.png",
|
||||
new: "Paintball.png"
|
||||
}
|
||||
];
|
||||
|
||||
const log = new Logging("ImageRoute");
|
||||
|
||||
async function convertImage(query: ImageQuery, data: Uint8Array<ArrayBufferLike>): Promise<Uint8Array<ArrayBufferLike> | null> {
|
||||
try {
|
||||
const image = sharp(data);
|
||||
const rootMetadata = await image.metadata();
|
||||
const squareSize = Math.min(rootMetadata.width, rootMetadata.height);
|
||||
if (query.cropSquare) image.resize(squareSize, squareSize);
|
||||
const image = await Image.decode(data);
|
||||
|
||||
const newImage = sharp(await image.png().toBuffer());
|
||||
if (query.width && query.height)
|
||||
newImage.resize(query.width, query.height);
|
||||
if (typeof query.height == 'number' && query.height >= image.height) query.height = image.height;
|
||||
if (typeof query.width == 'number' && query.width >= image.width) query.width = image.width;
|
||||
if (query.cropSquare) {
|
||||
|
||||
if (image.height > image.width) image.cover(image.height, image.height);
|
||||
else image.cover(image.width, image.width);
|
||||
}
|
||||
|
||||
if (query.width && query.height) {
|
||||
if (image.height > image.width) {
|
||||
image.resize(query.width, -1);
|
||||
image.cover(query.width, query.height);
|
||||
} else {
|
||||
image.resize(-1, query.height);
|
||||
image.cover(query.width, query.height);
|
||||
}
|
||||
}
|
||||
else if (query.width)
|
||||
newImage.resize(query.width);
|
||||
image.resize(query.width, -1);
|
||||
else if (query.height)
|
||||
newImage.resize(undefined, query.height);
|
||||
image.resize(-1, query.height);
|
||||
|
||||
return await newImage.png().toBuffer();
|
||||
return await image.encode(6);
|
||||
} catch (err) {
|
||||
log.w(`Image transformation failed: ${(err as Error).stack}`);
|
||||
return null;
|
||||
@@ -43,6 +66,7 @@ const imgQuerySchema = z.object({
|
||||
cropSquare: z.coerce.boolean().optional(),
|
||||
width: z.coerce.number().min(64).max(3840).optional(),
|
||||
height: z.coerce.number().min(64).max(2160).optional(),
|
||||
sig: z.literal('p1').optional()
|
||||
});
|
||||
type ImageQuery = z.infer<typeof imgQuerySchema>;
|
||||
|
||||
@@ -51,19 +75,22 @@ route.app.get('/:imgName',
|
||||
typedZValidator('query', imgQuerySchema),
|
||||
async c => {
|
||||
|
||||
const { imgName } = c.req.valid('param');
|
||||
const params = c.req.valid('param');
|
||||
const query = c.req.valid('query');
|
||||
|
||||
const redirect = redirects.find(red => red.org === params.imgName);
|
||||
if (redirect) params.imgName = redirect.new;
|
||||
|
||||
const datas: Uint8Array<ArrayBufferLike>[] = (await Promise.all<Uint8Array<ArrayBufferLike> | null>([
|
||||
new Promise(resolve => {
|
||||
Deno.readFile(path.join(RootPath, "res/baseimg/", imgName)).then(img => {
|
||||
Deno.readFile(path.join(RootPath, "res/baseimg/", params.imgName)).then(img => {
|
||||
resolve(img);
|
||||
}).catch(() => {
|
||||
resolve(null);
|
||||
});
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
Server.Content.getFile(`img/${imgName}`).then(file => {
|
||||
Server.Content.getFile(`img/${params.imgName}`).then(file => {
|
||||
if (file) resolve(file.Data);
|
||||
else resolve(null);
|
||||
}).catch(() => {
|
||||
@@ -72,6 +99,8 @@ route.app.get('/:imgName',
|
||||
})
|
||||
])).filter(val => val !== null);
|
||||
|
||||
if (query.sig) c.res.headers.set('content-signature', "key-id=KEY:RSA:p1.rec.net; data=aGk=");
|
||||
|
||||
if (datas.length == 0) return statusResponse(c, HTTPStatus.NotFound);
|
||||
else {
|
||||
const result = await convertImage(query, datas[0]);
|
||||
|
||||
@@ -26,7 +26,7 @@ export class PlatformsManager extends ServerContentBase {
|
||||
async getToken(prof: Profile, type: TokenType) {
|
||||
const secret = env["SECRET"];
|
||||
if (!secret) throw new Error("No SECRET in env. Did you forget to set it?");
|
||||
const exp = type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 31_556_952;
|
||||
const exp = type == TokenType.Access ? Math.round(Date.now() / 1000) + 21_600 : Math.round(Date.now() / 1000) + 21_600;
|
||||
|
||||
const token: TokenFormat = {
|
||||
typ: type,
|
||||
|
||||
@@ -2,10 +2,8 @@ export enum TokenType {
|
||||
Access,
|
||||
Refresh
|
||||
}
|
||||
export interface TokenFormatBase {
|
||||
export interface TokenFormat {
|
||||
typ: TokenType
|
||||
}
|
||||
export interface TokenFormat extends TokenFormatBase {
|
||||
iss: string,
|
||||
exp: number,
|
||||
iat: number,
|
||||
|
||||
@@ -13,7 +13,7 @@ export class ProfileMatchmakingManager extends ProfileContentManager {
|
||||
|
||||
#loginLockKey = this.profile.constructProfilePropertyKey('loginlock');
|
||||
async setLoginLock(lock: string) {
|
||||
await this.kv.getKv().set(this.#deviceClassKey, lock);
|
||||
await this.kv.getKv().set(this.#loginLockKey, lock);
|
||||
}
|
||||
async getLoginLock(): Promise<string | null> {
|
||||
return (await this.kv.getKv().get<string>(this.#loginLockKey)).value;
|
||||
|
||||
@@ -4,8 +4,9 @@ import Profile from "./profile.ts";
|
||||
import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
import Command from "./../commands/command.ts";
|
||||
import z from "zod";
|
||||
import { PlatformMask, ProfileRole, TokenType } from "../platforms/types.ts";
|
||||
import { PlatformMask, PlatformType, ProfileRole, TokenType } from "../platforms/types.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { BitFlags } from "../../util/flags.ts";
|
||||
|
||||
const profiles: Map<number, Profile> = new Map();
|
||||
|
||||
@@ -45,7 +46,7 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
else return await this.#getUnusedUsername();
|
||||
}
|
||||
|
||||
async create(platform: number, platformId: string, username?: string, id?: number) {
|
||||
async create(platform: PlatformType, mask: BitFlags<PlatformMask>, platformId: string, username?: string, id?: number) {
|
||||
if (typeof id == 'number') {
|
||||
const prof = await this.get(id);
|
||||
if (prof) throw new Error("ID is in use");
|
||||
@@ -60,14 +61,14 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
accountId: newId,
|
||||
username: newUsername,
|
||||
displayName: newUsername,
|
||||
platforms: PlatformMask.None,
|
||||
platforms: mask.getValue(),
|
||||
profileImage: "DefaultProfileImage.png",
|
||||
createdAt: new Date()
|
||||
}
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newId ], newProfile);
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newUsername ], newId);
|
||||
|
||||
await this.server.Platforms.addCachedLogin(platform, platformId, newId);
|
||||
if (platformId !== "") await this.server.Platforms.addCachedLogin(platform, platformId, newId);
|
||||
|
||||
return this.get(newId);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
if (agrooms.value !== null) this.#agroomIds = agrooms.value;
|
||||
this.#log.i(`${this.#agroomIds.size} AG rooms exist`);
|
||||
|
||||
if (this.getAgRoomIds().size === 0) {
|
||||
try {
|
||||
const config = await this.tryGetAgRoomRuntimeConfig();
|
||||
this.#log.i(`Starting AG room initialization`);
|
||||
this.initBuiltinRooms(config.Rooms, config.Locations);
|
||||
} catch (err) {
|
||||
this.#log.e(`Could not run AG room initialization: ${(err as Error).stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
const baserooms = await this.kv.getKv().get<Set<number>>([ServerRoomsBase.baseRoomIdsKey]);
|
||||
if (baserooms.value !== null) this.#baseroomIds = baserooms.value;
|
||||
|
||||
@@ -52,6 +62,8 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
key: ["initag", "initagrooms", "initagroom", "iag"],
|
||||
zod: z.tuple([]).rest(z.string()),
|
||||
exec: async (...arrayPath: string[]) => {
|
||||
if (this.#agroomIds.size !== 0) return new Error("AG rooms already initialized");
|
||||
|
||||
const path = arrayPath.join(' ');
|
||||
try {
|
||||
const config = JSON.parse((await Deno.readTextFile(path)).toString()) as AGRoomRuntimeConfig;
|
||||
@@ -105,6 +117,9 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
return this.#agRoomRuntimeConfig;
|
||||
}
|
||||
|
||||
async tryGetAgRoomRuntimeConfig() {
|
||||
return JSON.parse(await Deno.readTextFile(`${RootPath}/res/rooms.json`)) as AGRoomRuntimeConfig;
|
||||
}
|
||||
async initBuiltinRooms(rooms: AGRoom[], locations: AGRoomLocation[]) {
|
||||
await Promise.all(rooms.map(async room => {
|
||||
if (room.Accessibility == RoomDataTypes.RoomAccessibility.Private) return;
|
||||
@@ -115,10 +130,7 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
"ARRoom",
|
||||
"Registration",
|
||||
"DormRoom"
|
||||
].includes(room.Name)) {
|
||||
this.#log.w(`Room '${room.Name}' is not eligible for builtin room generation`);
|
||||
return;
|
||||
}
|
||||
].includes(room.Name)) return;
|
||||
|
||||
const roomFactory = await this.write();
|
||||
if (roomFactory == null) {
|
||||
@@ -185,9 +197,9 @@ export class ServerRoomsBase extends ServerContentBase {
|
||||
if (builtinScene) return builtinScene.SupportsJoinInProgress;
|
||||
else {
|
||||
if (!this.#joinInProgressLookup) throw new Error("JoinInProgress lookup table is not yet initialized");
|
||||
const lookup = this.#joinInProgressLookup[subroomFactory.RoomSceneLocationId];
|
||||
if (lookup) return lookup;
|
||||
else return false;
|
||||
const lookup = this.#joinInProgressLookup[subroomFactory.RoomSceneLocationId];
|
||||
if (lookup) return lookup;
|
||||
else return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,37 +10,42 @@
|
||||
export class BitFlags<T extends number> {
|
||||
private value: T;
|
||||
|
||||
constructor(initialValue: T | number = 0 as T) {
|
||||
this.value = initialValue as T;
|
||||
constructor(...initialValue: (T | number)[]) {
|
||||
if (Array.isArray(initialValue)) {
|
||||
// Combine multiple flags into one numeric value
|
||||
this.value = initialValue.reduce((acc, flag) => acc | flag, 0) as T;
|
||||
} else {
|
||||
this.value = initialValue as T;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get current numeric value */
|
||||
public getValue(): T {
|
||||
public getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/** Check if a specific flag is set */
|
||||
public has(flag: T): boolean {
|
||||
public has(flag: T) {
|
||||
return (this.value & flag) === flag;
|
||||
}
|
||||
|
||||
/** Add a flag */
|
||||
public add(flag: T): void {
|
||||
public add(flag: T) {
|
||||
this.value = (this.value | flag) as T;
|
||||
}
|
||||
|
||||
/** Remove a flag */
|
||||
public remove(flag: T): void {
|
||||
public remove(flag: T) {
|
||||
this.value = (this.value & ~flag) as T;
|
||||
}
|
||||
|
||||
/** Toggle a flag */
|
||||
public toggle(flag: T): void {
|
||||
public toggle(flag: T) {
|
||||
this.value = (this.value ^ flag) as T;
|
||||
}
|
||||
|
||||
/** Reset all flags */
|
||||
public clear(): void {
|
||||
public clear() {
|
||||
this.value = 0 as T;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ import { z } from "zod";
|
||||
import type { HonoEnv } from "./types.ts";
|
||||
import { ZodSchema } from "zod/v4";
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const log = new Logging("Validation");
|
||||
|
||||
// thanks claude, this hurt my brain!
|
||||
|
||||
@@ -17,7 +20,10 @@ export const typedZValidator = <
|
||||
recNetError?: string
|
||||
) => {
|
||||
return zValidator(target, schema, (result, c) => {
|
||||
|
||||
if (!result.success) {
|
||||
log.w(`Validation failed: ${JSON.stringify(result.error)}`);
|
||||
|
||||
if (recNetError) return c.json({ success: false, error: recNetError }, httpOk == true ? HTTPStatus.OK : HTTPStatus.BadRequest);
|
||||
else return c.json({ success: false, error: "Request validation failed" }, httpOk == true ? HTTPStatus.OK : HTTPStatus.BadRequest);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user