import { Firestore } from "firebase-admin/firestore"; import * as agc from "../agc"; import * as akashic from "../akashic"; import { AkashicError } from "../akashic"; import * as resolvers from "../resolvers"; import * as stores from "../stores"; export interface AkashicParameters { mode: "agc" | "akashic"; baseUrl: string; apiKey?: string; refreshToken?: string; refreshTokenUrl?: string; firestore?: Firestore; } export class AkashicWrapper { readonly agcClient?: agc.Client; readonly akashicClient?: akashic.Client; readonly mode: "agc" | "akashic"; idToken?: string; refreshToken?: string; refreshTokenUrl?: string; firestore?: Firestore; constructor(params: AkashicParameters) { if (params.mode === "akashic") { if (params.refreshToken == null) { throw new Error("AkashicモードでrefreshTokenを省略することはできません。"); } if (params.refreshTokenUrl == null) { throw new Error("AkashicモードでrefreshTokenUrlを省略することはできません。"); } } if (params.mode === "agc") { this.agcClient = new agc.Client({ baseUrl: params.baseUrl, apiKey: params.apiKey, }); } else { this.akashicClient = new akashic.Client(params.baseUrl); } this.mode = params.mode; this.refreshToken = params.refreshToken; this.refreshTokenUrl = params.refreshTokenUrl; this.firestore = params.firestore; this.idToken = undefined; } getClient() { if (this.agcClient == null) return this.akashicClient!; return this.agcClient; } async resolveContentUrl(gameCode: string) { if (this.mode === "akashic") { return resolvers.contents.resolveUrl(gameCode); } const content = await agc.api.contents.get(this.getClient() as agc.Client, gameCode); return content.data.url; } async getPlaylog(playId: string) { if (this.mode === "akashic") { const result = await this.wrap((akashicClient) => { return akashic.api.playPlaylog.get(akashicClient!, playId); }); return { playlog: result.data, }; } throw new Error("invalid call api"); } async createPlayToken(playId: string, userId: string) { if (this.mode === "akashic") { const createTokenResult = await this.wrap((akashicClient) => { return akashic.api.playTokens.create(akashicClient!, playId, { permission: { readTick: true, subscribeEvent: false, sendEvent: true, maxEventPriority: 0, writeTick: false, subscribeTick: true, }, userId, }); }); return { serverUrl: createTokenResult.data.url, token: createTokenResult.data.value, }; } const createAgcTokenResult = await agc.api.plays.createToken(this.agcClient!, playId, userId); return createAgcTokenResult.data; } async createPlay( gameCode: string, contentUrl: string, handlerBaseUrl: string, args?: any, initialEvent?: akashic.PlayEventParams ) { if (this.mode === "agc") { const createResult = await agc.api.contentPlays.create(this.agcClient!, gameCode); if (initialEvent != null) { await agc.api.playEvents.create(this.agcClient!, createResult.data.playId, initialEvent); } return createResult.data; } const playApiResult = await this.wrap((akashicClient) => { return akashic.api.plays.create(akashicClient, { gameCode, }); }); const playId = playApiResult.data.id; // TODO: ここで必要に応じてraiseEvent(多分必要) // TODO: EventHandlers考えてなかったので後日考える if (initialEvent != null) { await this.wrap((akashicClient) => { return akashic.api.playEvents.create(akashicClient, playId, initialEvent); }); } const handlers = resolvers.handlers.get(handlerBaseUrl, gameCode); const instanceApiResult = await this.wrap((akashicClient) => { return akashic.api.instances.create(akashicClient, { gameCode, modules: [ { code: "dynamicPlaylogWorker", values: { executionMode: "active", playId: playApiResult.data.id, }, }, { code: "akashicEngineParameters", values: { contentJsonUrl: contentUrl, args, }, }, { code: "eventHandlers", values: { handlers, }, }, ], cost: 1, }); }); return { playId, instanceId: instanceApiResult.data.id, }; } async stopInstance(id: string) { const resInstances = await this.wrap((akashicClient) => { return akashic.api.plays.getInstances(akashicClient, id); }); const stoppedInstanceStatus = ["closed", "closing", "error"]; const instances = resInstances.data.values.filter( (instance) => !stoppedInstanceStatus.includes(instance.status) ); for (let i = 0; i < instances.length; i++) { try { await this.wrap((akashicClient) => { return akashic.api.instances.stop(akashicClient, instances[i].id); }); } catch (error) { const apiError = error as AkashicError; // すでに終了している409は無視する if (apiError.response.status !== 409) { throw error; } } } try { await this.wrap((akashicClient) => { return akashic.api.plays.stop(akashicClient, id); }); } catch (error) { const apiError = error as AkashicError; // すでに終了している409は無視する if (apiError.response.status !== 409) { throw error; } } } async sendEvent(playId: string, event: akashic.PlayEventParams) { if (this.mode === "agc") { const playEventResult = await agc.api.playEvents.create(this.agcClient!, playId, event); return playEventResult; } const result = await this.wrap((akashicClient) => { return akashic.api.playEvents.create(akashicClient, playId, event); }); return result; } async regenerateIdToken() { const refreshTokenClient = new akashic.Client(this.refreshTokenUrl!); const authTokenResult = await akashic.api.tokens.create(refreshTokenClient, this.refreshToken!); if (this.firestore != null) { await stores.storeAkashicTokenProperty(this.firestore, authTokenResult); } return authTokenResult.idToken; } async wrap(callback: (akashiClient: akashic.Client) => Promise) { if (this.idToken == null) { try { if (this.firestore != null) { const akashicAuthToken = await resolvers.System.resolveAkashicToken(this.firestore); this.idToken = akashicAuthToken?.idToken; } } catch { // ここで無い場合、下のregenerateに行くのでこの例外は無視 } } for (let i = 0; i < 2; i++) { if (this.idToken == null) { this.idToken = await this.regenerateIdToken(); } const client = this.akashicClient!; client.baseHeaders["Authorization"] = this.idToken; try { const result = await callback(client); return result; } catch (error) { if (error instanceof akashic.PermissionDeniedError) { // retry this.idToken = await this.regenerateIdToken(); continue; } throw error; } } throw new Error("Can not call api: retry limit"); } } export function createWrapper(params: AkashicParameters) { return new AkashicWrapper(params); }