import { zValidator } from "@hono/zod-validator"; import { Client } from "@upstash/qstash"; import { Hono, type HonoRequest } from "hono"; import { env } from "hono/adapter"; import mapKeys from "lodash.mapkeys"; import { z } from "zod"; import { Case, changeStringCase } from "~/libs/changeStringCase"; import type { AdminSdkCredentials } from "~/libs/fcm/getGoogleAuthToken"; import { sendFcmMessage } from "~/libs/fcm/sendFcmMessage"; import { getCurrentDomain } from "~/libs/getCurrentDomain"; import { maybeScheduleNextAiringEpisode } from "~/libs/maybeScheduleNextAiringEpisode"; import { verifyQstashHeader } from "~/libs/qstash/verifyQstashHeader"; import { readEnvVariable } from "~/libs/readEnvVariable"; import { getTokensSubscribedToTitle } from "~/models/token"; import type { Env } from "~/types/env"; import type { EpisodesResponseSchema } from "~/types/episode"; import type { FetchUrlResponse } from "~/types/episode/fetch-url-response"; import { AniListIdSchema, EpisodeNumberSchema, ErrorResponse, SuccessResponse, } from "~/types/schema"; const app = new Hono(); app.post( "/", zValidator( "json", z.object({ aniListId: AniListIdSchema, episodeNumber: EpisodeNumberSchema, }), ), async (c) => { const { aniListId, episodeNumber } = await c.req.json<{ aniListId: number; episodeNumber: number; }>(); console.log( `Internal new episode route, aniListId: ${aniListId}, episodeNumber: ${episodeNumber}`, ); if (!(await verifyQstashHeader(env(c, "workerd"), c.req))) { return c.json(ErrorResponse, { status: 401 }); } const domain = getCurrentDomain(c.req); const { success, result: fetchEpisodesResult } = await fetch( `${domain}/episodes/${aniListId}`, ).then((res) => res.json()); if (!success) { await scheduleRetry( readEnvVariable(env(c, "workerd"), "QSTASH_TOKEN"), c.req, ); return c.json(ErrorResponse, { status: 500 }); } const { episodes, providerId } = fetchEpisodesResult; const episode = episodes.find( (episode) => episode.number === episodeNumber, ); if (!episode) { await scheduleRetry( readEnvVariable(env(c, "workerd"), "QSTASH_TOKEN"), c.req, ); return c.json(ErrorResponse, { status: 404 }); } const { success: fetchUrlSuccess, result: fetchUrlResult } = await fetch( `${domain}/episodes/${aniListId}/url`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ id: episode.id, provider: providerId, }), }, ).then((res) => res.json()); if (!fetchUrlSuccess) { await scheduleRetry( readEnvVariable(env(c, "workerd"), "QSTASH_TOKEN"), c.req, ); return c.json(ErrorResponse, { status: 500 }); } const tokens = await getTokensSubscribedToTitle( env(c, "workerd"), aniListId, ); await Promise.all( tokens.map(async (token) => { return sendFcmMessage( mapKeys( readEnvVariable( env(c, "workerd"), "ADMIN_SDK_JSON", ), (_, key) => changeStringCase(key, Case.snake_case, Case.camelCase), ) as unknown as AdminSdkCredentials, { token, data: { type: "new_episode", episodes: JSON.stringify(episodes), episodeStreamInfo: JSON.stringify(fetchUrlResult), aniListId: aniListId.toString(), episodeNumber: episodeNumber.toString(), }, android: { priority: "high" }, }, ); }), ); await maybeScheduleNextAiringEpisode( env(c, "workerd"), c.req, aniListId, ); return c.json(SuccessResponse, 200); }, ); async function scheduleRetry(qstashToken: string, req: HonoRequest) { return new Client({ token: qstashToken }).publishJSON({ body: await req.json(), url: req.url, retries: 0, delay: "1h", }); } export default app;