mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
import { Hono } from 'hono'
|
|
import { useWorkersLogger } from 'workers-tagged-logger'
|
|
|
|
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
|
|
|
import type { App } from './context'
|
|
import type { Context } from 'hono'
|
|
import { validateAndGetAccountId } from './jwt'
|
|
|
|
/**
|
|
* Ported from the C# `MatchmakingController`. Endpoints the C# backs with EF Core
|
|
* (`AppDbContext`) are stubbed here — there's no DB binding yet, so room/player
|
|
* lookups fall back to the same defaults the C# uses when nothing is found.
|
|
*
|
|
* Auth-gated routes still validate the Bearer JWT issued by the `auth` worker.
|
|
*/
|
|
|
|
/**
|
|
* Default `/player` payload. The C# serves this from `JSON/getplayer.json`
|
|
* whenever the `id` is missing/invalid or the account isn't found; Workers have
|
|
* no filesystem so it's inlined here.
|
|
*/
|
|
const DEFAULT_GET_PLAYER = [
|
|
{
|
|
playerId: 1,
|
|
statusVisibility: 0,
|
|
deviceClass: 0,
|
|
vrMovementMode: 1,
|
|
roomInstance: null,
|
|
isOnline: true,
|
|
appVersion: '20210129',
|
|
platform: 0,
|
|
},
|
|
]
|
|
|
|
/** Heartbeat body posted by the client (all fields optional). */
|
|
interface HeartbeatRequest {
|
|
playerId?: number
|
|
statusVisibility?: number
|
|
deviceClass?: number
|
|
vrMovementMode?: number
|
|
appVersion?: string | null
|
|
platform?: number
|
|
}
|
|
|
|
/**
|
|
* Resolve the account id from a Bearer token, mirroring the repeated
|
|
* auth-header check in the C#. Returns `null` when the header is missing,
|
|
* the token is invalid, or the `sub` claim isn't an integer.
|
|
*/
|
|
async function authedId(c: Context<App>): Promise<number | null> {
|
|
const authHeader = c.req.header('Authorization') ?? ''
|
|
if (!authHeader.toLowerCase().startsWith('bearer ')) return null
|
|
|
|
const token = authHeader.slice('Bearer '.length)
|
|
const accountId = await validateAndGetAccountId(token)
|
|
if (!accountId) return null
|
|
|
|
const id = Number.parseInt(accountId, 10)
|
|
return Number.isNaN(id) ? null : id
|
|
}
|
|
|
|
/** Results.Unauthorized() equivalent — 401 with empty body. */
|
|
function unauthorized(c: Context<App>) {
|
|
return c.body(null, 401)
|
|
}
|
|
|
|
const app = new Hono<App>()
|
|
.use(
|
|
'*',
|
|
// middleware
|
|
(c, next) =>
|
|
useWorkersLogger(c.env.NAME, {
|
|
environment: c.env.ENVIRONMENT,
|
|
release: c.env.SENTRY_RELEASE,
|
|
})(c, next)
|
|
)
|
|
|
|
.onError(withOnError())
|
|
.notFound(withNotFound())
|
|
|
|
// ---- Player presence -----------------------------------------------------
|
|
.post('/player/login', (c) => c.body(null, 200))
|
|
|
|
.get('/player', (c) => {
|
|
// C# loads the account + its active RoomInstance; without a DB binding it
|
|
// always falls through to the JSON/getplayer.json default.
|
|
// TODO: build the per-account payload once a DB binding exists.
|
|
return c.json(DEFAULT_GET_PLAYER)
|
|
})
|
|
|
|
.post('/player/heartbeat', async (c) => {
|
|
const id = await authedId(c)
|
|
if (id === null) return unauthorized(c)
|
|
|
|
// Body may be a JSON HeartbeatRequest or a form post; only JSON is read.
|
|
const raw = await c.req.text().catch(() => '')
|
|
let hb: HeartbeatRequest = {}
|
|
if (raw.trimStart().startsWith('{')) {
|
|
try {
|
|
hb = JSON.parse(raw) as HeartbeatRequest
|
|
} catch {
|
|
hb = {}
|
|
}
|
|
}
|
|
|
|
// TODO: look up the player's active RoomInstance once a DB binding exists.
|
|
return c.json({
|
|
playerId: hb.playerId ? hb.playerId : id,
|
|
statusVisibility: hb.statusVisibility ?? 0,
|
|
deviceClass: hb.deviceClass ?? 0,
|
|
vrMovementMode: hb.vrMovementMode ? hb.vrMovementMode : 1,
|
|
roomInstance: null,
|
|
isOnline: false,
|
|
appVersion: hb.appVersion ?? '',
|
|
platform: hb.platform ?? 0,
|
|
})
|
|
})
|
|
|
|
.put('/player/statusvisibility', (c) => c.body(null, 200)) // TODO: add functionality
|
|
|
|
// ---- Room navigation -----------------------------------------------------
|
|
.post('/goto/room/:room', async (c) => {
|
|
const id = await authedId(c)
|
|
if (id === null) return unauthorized(c)
|
|
// No Rooms binding → the room can never be found (C# returns NotFound here).
|
|
// TODO: resolve the Room, upsert a RoomInstance, and return it.
|
|
return c.text('Room not found', 404)
|
|
})
|
|
|
|
.post('/goto/none', (c) =>
|
|
// Offline dorm — fully static in the C# source.
|
|
c.json({
|
|
errorCode: 0,
|
|
roomInstance: {
|
|
roomInstanceId: 1,
|
|
roomId: 1,
|
|
subRoomId: 1,
|
|
roomInstanceType: 2,
|
|
location: '76d98498-60a1-430c-ab76-b54a29b7a163',
|
|
dataBlob: '',
|
|
eventId: 0,
|
|
clubId: 0,
|
|
photonRegionId: 'us',
|
|
photonRoomId: crypto.randomUUID(),
|
|
name: 'DormRoom',
|
|
maxCapacity: 4,
|
|
isFull: false,
|
|
isPrivate: true,
|
|
isInProgress: false,
|
|
EncryptVoiceChat: false,
|
|
},
|
|
})
|
|
)
|
|
|
|
// ---- Room instance -------------------------------------------------------
|
|
.post('/roominstance/:id/reportjoinresult', (c) => c.body(null, 200))
|
|
|
|
export default app
|