up
This commit is contained in:
+103
-144
@@ -1,3 +1,5 @@
|
||||
import { createEveningDetectiveServerClient } from './api/generated/crabs/evening_detective_server/index.js'
|
||||
import { apiScreenMode, apiScreenCommand, apiPlan } from './api-values.js'
|
||||
import { ApiError, NetworkError, SessionChangedError, networkFailure } from './request-error.js'
|
||||
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
@@ -449,12 +451,9 @@ export class Online {
|
||||
const version = this.planVersion
|
||||
this.lastPlan = Date.now()
|
||||
try {
|
||||
const result = await this.api(
|
||||
`/api/games/${pkg.package.game.id}/screen-plan`,
|
||||
undefined,
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
const result = await this.client(true, AbortSignal.timeout(5000)).GetScreenPlan({
|
||||
gameID: pkg.package.game.id,
|
||||
})
|
||||
await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version)
|
||||
} catch (error) {
|
||||
if (generation === this.generation && version === this.planVersion)
|
||||
@@ -472,13 +471,10 @@ export class Online {
|
||||
const version = ++this.planVersion
|
||||
this.planBusy++
|
||||
try {
|
||||
const result = await this.json(
|
||||
await this.authorized(`/api/games/${pkg.package.game.id}/screen-plan`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(plan),
|
||||
}),
|
||||
)
|
||||
const result = await this.client().SaveScreenPlan({
|
||||
gameID: pkg.package.game.id,
|
||||
...apiPlan(plan),
|
||||
})
|
||||
if (generation !== this.generation) throw new SessionChangedError()
|
||||
await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version)
|
||||
} finally {
|
||||
@@ -576,12 +572,10 @@ export class Online {
|
||||
return
|
||||
}
|
||||
if (intent.action === 'step') {
|
||||
const result = await this.api(
|
||||
`/api/games/${pkg.package.game.id}/screen-plan/step`,
|
||||
{ currentStep: intent.index },
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
const result = await this.client(true, AbortSignal.timeout(5000)).SetScreenPlanStep({
|
||||
gameID: pkg.package.game.id,
|
||||
currentStep: intent.index,
|
||||
})
|
||||
const confirmed = parsePlan(result.plan)
|
||||
const shown = this.controller.state.catalog.steps[intent.index]
|
||||
await this.acceptPlan(confirmed, pkg.key, generation, version)
|
||||
@@ -599,29 +593,29 @@ export class Online {
|
||||
}
|
||||
} else if ('channel' in intent && intent.channel === 'music') {
|
||||
const c = localState.channels.music
|
||||
await this.api(
|
||||
`/api/games/${pkg.package.game.id}/music`,
|
||||
{
|
||||
playlistID: c.playlistId ?? 0,
|
||||
trackIndex: c.trackIndex,
|
||||
positionMs: Math.max(0, Math.round(c.position * 1000)),
|
||||
volume: Math.round(c.volume),
|
||||
loop: c.loop,
|
||||
playback: c.desired === 'playing' ? 2 : c.desired === 'paused' ? 3 : 1,
|
||||
},
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
await this.client(true, AbortSignal.timeout(5000)).SetGameMusic({
|
||||
gameID: pkg.package.game.id,
|
||||
playlistID: c.playlistId ?? 0,
|
||||
trackIndex: c.trackIndex,
|
||||
positionMs: Math.max(0, Math.round(c.position * 1000)),
|
||||
volume: Math.round(c.volume),
|
||||
loop: c.loop,
|
||||
playback:
|
||||
c.desired === 'playing'
|
||||
? 'MUSIC_PLAYBACK_PLAYING'
|
||||
: c.desired === 'paused'
|
||||
? 'MUSIC_PLAYBACK_PAUSED'
|
||||
: 'MUSIC_PLAYBACK_STOPPED',
|
||||
})
|
||||
} else if (intent.action === 'mode' || intent.action === 'select') {
|
||||
await this.api(
|
||||
`/api/games/${pkg.package.game.id}/screens/mode`,
|
||||
{ mode: localState.scene.mode },
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
await this.client(true, AbortSignal.timeout(5000)).SetScreenMode({
|
||||
gameID: pkg.package.game.id,
|
||||
mode: apiScreenMode(localState.scene.mode),
|
||||
})
|
||||
} else if (intent.action === 'text') {
|
||||
const path = `/api/games/${pkg.package.game.id}/screen-plan`
|
||||
const current = parsePlan((await this.api(path)).plan)
|
||||
const current = parsePlan(
|
||||
(await this.client().GetScreenPlan({ gameID: pkg.package.game.id })).plan,
|
||||
)
|
||||
if (
|
||||
generation !== this.generation ||
|
||||
session !== this.controller.state.session ||
|
||||
@@ -637,13 +631,7 @@ export class Online {
|
||||
timerLabel: '',
|
||||
timerDurationSeconds: 0,
|
||||
})
|
||||
await this.json(
|
||||
await this.authorized(path, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(current),
|
||||
}),
|
||||
)
|
||||
await this.client().SaveScreenPlan({ gameID: pkg.package.game.id, ...apiPlan(current) })
|
||||
}
|
||||
if (
|
||||
generation !== this.generation ||
|
||||
@@ -651,7 +639,10 @@ export class Online {
|
||||
version !== this.planVersion
|
||||
)
|
||||
return
|
||||
const result = await this.api(`${path}/step`, { currentStep: index })
|
||||
const result = await this.client().SetScreenPlanStep({
|
||||
gameID: pkg.package.game.id,
|
||||
currentStep: index,
|
||||
})
|
||||
await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version)
|
||||
if (
|
||||
generation !== this.generation ||
|
||||
@@ -661,21 +652,18 @@ export class Online {
|
||||
return
|
||||
this.controller.markStep(index)
|
||||
} else if (intent.action === 'timer-set') {
|
||||
await this.api(
|
||||
`/api/games/${pkg.package.game.id}/screens/timer`,
|
||||
{ label: intent.label, durationSeconds: intent.durationSeconds },
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
await this.client(true, AbortSignal.timeout(5000)).SetScreenTimer({
|
||||
gameID: pkg.package.game.id,
|
||||
label: intent.label,
|
||||
durationSeconds: intent.durationSeconds,
|
||||
})
|
||||
} else {
|
||||
const index = this.controller.state.scene.stepIndex
|
||||
if (index !== null && index !== this.plan.currentStep) {
|
||||
const result = await this.api(
|
||||
`/api/games/${pkg.package.game.id}/screen-plan/step`,
|
||||
{ currentStep: index },
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
const result = await this.client(true, AbortSignal.timeout(5000)).SetScreenPlanStep({
|
||||
gameID: pkg.package.game.id,
|
||||
currentStep: index,
|
||||
})
|
||||
await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version)
|
||||
if (
|
||||
generation !== this.generation ||
|
||||
@@ -699,16 +687,12 @@ export class Online {
|
||||
}
|
||||
if (!(intent.action in commands)) throw new Error('Некорректная команда плана')
|
||||
expectedCommand = commands[intent.action as keyof typeof commands]
|
||||
await this.api(
|
||||
`/api/games/${pkg.package.game.id}/screens/command`,
|
||||
{
|
||||
command: expectedCommand,
|
||||
positionMs: 'position' in intent ? Math.round(intent.position * 1000) : 0,
|
||||
volume: 'volume' in intent ? intent.volume : this.controller.state.channels.main.volume,
|
||||
},
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
await this.client(true, AbortSignal.timeout(5000)).SendScreenCommand({
|
||||
gameID: pkg.package.game.id,
|
||||
command: apiScreenCommand(expectedCommand),
|
||||
positionMs: 'position' in intent ? Math.round(intent.position * 1000) : 0,
|
||||
volume: 'volume' in intent ? intent.volume : this.controller.state.channels.main.volume,
|
||||
})
|
||||
}
|
||||
if (
|
||||
generation !== this.generation ||
|
||||
@@ -719,12 +703,10 @@ export class Online {
|
||||
this.planNotice = ''
|
||||
// Ответ следующего опроса становится базой: собственная команда не применяется дважды.
|
||||
if (this.screen) {
|
||||
const result = await this.api(
|
||||
'/api/screens/state',
|
||||
{ screenID: this.screen.id, accessToken: this.screen.token },
|
||||
false,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
const result = await this.client(false, AbortSignal.timeout(5000)).GetScreenState({
|
||||
screenID: this.screen.id,
|
||||
accessToken: this.screen.token,
|
||||
})
|
||||
if (generation !== this.generation || session !== this.controller.state.session) return
|
||||
const remote = parseRemote(result.state)
|
||||
if (remote.gameId !== pkg.package.game.id) throw new Error('Привязка экрана изменилась')
|
||||
@@ -748,12 +730,9 @@ export class Online {
|
||||
needsMainSync = !isMusicCommand
|
||||
}
|
||||
if (needsMainSync && remote.command === 0) {
|
||||
const result = await this.api(
|
||||
`/api/games/${pkg.package.game.id}/screen-plan`,
|
||||
undefined,
|
||||
true,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
const result = await this.client(true, AbortSignal.timeout(5000)).GetScreenPlan({
|
||||
gameID: pkg.package.game.id,
|
||||
})
|
||||
await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version)
|
||||
if (
|
||||
generation !== this.generation ||
|
||||
@@ -1021,19 +1000,18 @@ export class Online {
|
||||
if (this.refreshPromise) return this.refreshPromise
|
||||
const c = this.credentials!
|
||||
this.refreshPromise = (async () => {
|
||||
const response = await this.raw(c.server, '/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken: c.refresh }),
|
||||
})
|
||||
if (generation !== this.generation) throw new SessionChangedError()
|
||||
if (response.status === 401) {
|
||||
this.credentials = null
|
||||
this.state.authenticated = false
|
||||
await this.persistSecrets()
|
||||
throw new ApiError(401)
|
||||
let result
|
||||
try {
|
||||
result = await this.client(false, undefined, c.server).Refresh({ refreshToken: c.refresh })
|
||||
} catch (error) {
|
||||
if (generation !== this.generation) throw new SessionChangedError()
|
||||
if (error instanceof ApiError && error.status === 401) {
|
||||
this.credentials = null
|
||||
this.state.authenticated = false
|
||||
await this.persistSecrets()
|
||||
}
|
||||
throw error
|
||||
}
|
||||
const result = await this.json(response)
|
||||
if (generation !== this.generation) throw new SessionChangedError()
|
||||
this.credentials = {
|
||||
...c,
|
||||
@@ -1070,21 +1048,19 @@ export class Online {
|
||||
}
|
||||
return response
|
||||
}
|
||||
private async api(path: string, body?: object, authorized = true, signal?: AbortSignal) {
|
||||
const init: RequestInit =
|
||||
body === undefined
|
||||
? { signal }
|
||||
: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
}
|
||||
return this.json(
|
||||
authorized
|
||||
? await this.authorized(path, init)
|
||||
: await this.raw(this.state.server, path, init),
|
||||
)
|
||||
private client(authorized = true, signal?: AbortSignal, server = this.state.server) {
|
||||
return createEveningDetectiveServerClient(async (request) => {
|
||||
const init: RequestInit = {
|
||||
method: request.method,
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: request.body,
|
||||
signal,
|
||||
}
|
||||
const path = `/${request.path}`
|
||||
return this.json(
|
||||
authorized ? await this.authorized(path, init) : await this.raw(server, path, init),
|
||||
)
|
||||
})
|
||||
}
|
||||
async act(input: unknown): Promise<OnlineState> {
|
||||
const data = object(input)
|
||||
@@ -1127,12 +1103,7 @@ export class Online {
|
||||
this.state.games = []
|
||||
await this.persistSecrets()
|
||||
this.state.server = server
|
||||
const response = await this.raw(server, '/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
const result = await this.json(response)
|
||||
const result = await this.client(false, undefined, server).Login({ email, password })
|
||||
this.credentials = {
|
||||
server,
|
||||
access: token(result.accessToken),
|
||||
@@ -1140,7 +1111,7 @@ export class Online {
|
||||
screen: null,
|
||||
}
|
||||
try {
|
||||
const me = object((await this.api('/api/users/me')).user)
|
||||
const me = object((await this.client().GetMe({})).user)
|
||||
if (
|
||||
!Array.isArray(me.roles) ||
|
||||
!me.roles.some((r) => ['admin', 'organizer', 'author'].includes(String(r)))
|
||||
@@ -1261,17 +1232,13 @@ export class Online {
|
||||
this.state.packagesRevision++
|
||||
await this.saveSelection()
|
||||
}
|
||||
const result = await this.api('/api/screens', {}, false)
|
||||
const result = await this.client(false).CreateScreenSession({})
|
||||
const s: ScreenSession = {
|
||||
id: String(integer(result.screenID, 1)),
|
||||
token: token(result.accessToken),
|
||||
gameId,
|
||||
}
|
||||
await this.api(`/api/screens/${s.id}/bind`, {
|
||||
screenID: s.id,
|
||||
accessToken: s.token,
|
||||
gameID: s.gameId,
|
||||
})
|
||||
await this.client().BindScreen({ screenID: s.id, accessToken: s.token, gameID: s.gameId })
|
||||
this.screen = s
|
||||
this.state.screenId = s.id
|
||||
this.state.connection = 'waiting'
|
||||
@@ -1285,10 +1252,7 @@ export class Online {
|
||||
case 'sound': {
|
||||
const generation = this.generation
|
||||
if (!this.screen) throw new Error('Сначала подключите управление')
|
||||
await this.api(`/api/screens/${this.screen.id}/sound`, {
|
||||
screenID: this.screen.id,
|
||||
soundEnabled: true,
|
||||
})
|
||||
await this.client().SetScreenSound({ screenID: this.screen.id, soundEnabled: true })
|
||||
if (generation === this.generation) this.state.soundEnabled = true
|
||||
return
|
||||
}
|
||||
@@ -1299,7 +1263,7 @@ export class Online {
|
||||
}
|
||||
}
|
||||
private async loadGames() {
|
||||
const result = await this.api('/api/games')
|
||||
const result = await this.client().GetGames({})
|
||||
if (!Array.isArray(result.games ?? [])) throw new Error('Некорректный список игр')
|
||||
this.state.games = ((result.games ?? []) as unknown[]).map((value) => {
|
||||
const g = object(value)
|
||||
@@ -1328,11 +1292,10 @@ export class Online {
|
||||
this.resultsBusy = true
|
||||
this.lastResults = Date.now()
|
||||
try {
|
||||
const result = await this.api(
|
||||
'/api/screens/results',
|
||||
{ screenID: s.id, accessToken: s.token },
|
||||
false,
|
||||
)
|
||||
const result = await this.client(false).GetScreenResults({
|
||||
screenID: s.id,
|
||||
accessToken: s.token,
|
||||
})
|
||||
if (generation !== this.generation || key !== this.controller.state.catalog.key) return
|
||||
if (!Array.isArray(result.questions ?? []) || !Array.isArray(result.teams ?? []))
|
||||
throw new Error('Некорректные результаты')
|
||||
@@ -1378,12 +1341,10 @@ export class Online {
|
||||
const version = this.planVersion
|
||||
this.pollBusy = true
|
||||
try {
|
||||
const result = await this.api(
|
||||
'/api/screens/state',
|
||||
{ screenID: s.id, accessToken: s.token },
|
||||
false,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
const result = await this.client(false, AbortSignal.timeout(5000)).GetScreenState({
|
||||
screenID: s.id,
|
||||
accessToken: s.token,
|
||||
})
|
||||
if (generation !== this.generation || version !== this.planVersion) return
|
||||
let next = parseRemote(result.state)
|
||||
if (next.gameId !== s.gameId) {
|
||||
@@ -1458,12 +1419,10 @@ export class Online {
|
||||
await this.synchronize()
|
||||
return
|
||||
}
|
||||
const result = await this.api(
|
||||
'/api/screens/music/advance',
|
||||
{ screenID: this.screen.id, accessToken: this.screen.token },
|
||||
false,
|
||||
AbortSignal.timeout(5000),
|
||||
)
|
||||
const result = await this.client(false, AbortSignal.timeout(5000)).AdvanceScreenMusic({
|
||||
screenID: this.screen.id,
|
||||
accessToken: this.screen.token,
|
||||
})
|
||||
if (generation !== this.generation || report.session !== this.controller.state.session) return
|
||||
const music = parseRemote({ music: result.state }).music
|
||||
if (this.received) {
|
||||
|
||||
Reference in New Issue
Block a user