diff --git a/index.html b/index.html index 3335369e..71b10ed7 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ - + @@ -59,4 +59,4 @@ - \ No newline at end of file + diff --git a/package.json b/package.json index d2a5dbb2..6a72405a 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react-i18next": "^12.1.1", "react-router-dom": "^5.2.0", "react-sticky-el": "^2.1.0", + "react-turnstile": "^1.1.2", "react-use": "^17.4.0", "slugify": "^1.6.6", "subsrt-ts": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2e88592..3be020b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ dependencies: react-sticky-el: specifier: ^2.1.0 version: 2.1.0(react-dom@17.0.2)(react@17.0.2) + react-turnstile: + specifier: ^1.1.2 + version: 1.1.2(react-dom@17.0.2)(react@17.0.2) react-use: specifier: ^17.4.0 version: 17.4.0(react-dom@17.0.2)(react@17.0.2) @@ -5321,6 +5324,16 @@ packages: react-dom: 17.0.2(react@17.0.2) dev: false + /react-turnstile@1.1.2(react-dom@17.0.2)(react@17.0.2): + resolution: {integrity: sha512-wfhSf4JtXlmLRkfxMryU8yEeCbh401muKoInhx+TegYwP8RprUW5XPZa8WnCNZiYpMy1i6IXAb1Ar7xj5HxJag==} + peerDependencies: + react: '>= 17.0.0' + react-dom: '>= 17.0.0' + dependencies: + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + dev: false + /react-universal-interface@0.6.2(react@17.0.2)(tslib@2.6.2): resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} peerDependencies: diff --git a/src/backend/helpers/providerApi.ts b/src/backend/helpers/providerApi.ts index affe387f..ed79bd48 100644 --- a/src/backend/helpers/providerApi.ts +++ b/src/backend/helpers/providerApi.ts @@ -1,8 +1,10 @@ import { MetaOutput, NotFoundError, ScrapeMedia } from "@movie-web/providers"; import { mwFetch } from "@/backend/helpers/fetch"; +import { getTurnstileToken, isTurnstileInitialized } from "@/stores/turnstile"; let metaDataCache: MetaOutput[] | null = null; +let token: null | string = null; export function setCachedMetadata(data: MetaOutput[]) { metaDataCache = data; @@ -12,6 +14,20 @@ export function getCachedMetadata(): MetaOutput[] { return metaDataCache ?? []; } +function getTokenIfValid(): null | string { + if (!token) return null; + const parts = token.split("."); + if (parts.length !== 3) return null; + try { + const parsedData = JSON.parse(atob(parts[2])); + if (!parsedData.exp) return token; + if (Date.now() < parsedData.exp) return token; + } catch { + // we dont care about parse errors + } + return null; +} + export async function fetchMetadata(base: string) { if (metaDataCache) return; const data = await mwFetch(`${base}/metadata`); @@ -67,8 +83,21 @@ export function makeProviderUrl(base: string) { }; } -export function connectServerSideEvents(url: string, endEvents: string[]) { - const eventSource = new EventSource(url); +export async function connectServerSideEvents( + url: string, + endEvents: string[] +) { + // fetch token to use + let apiToken = getTokenIfValid(); + if (!apiToken && isTurnstileInitialized()) { + apiToken = await getTurnstileToken(); + } + + // insert token, if its set + const parsedUrl = new URL(url); + if (apiToken) parsedUrl.searchParams.set("token", apiToken); + const eventSource = new EventSource(parsedUrl.toString()); + let promReject: (reason?: any) => void; let promResolve: (value: T) => void; const promise = new Promise((resolve, reject) => { @@ -83,6 +112,10 @@ export function connectServerSideEvents(url: string, endEvents: string[]) { }); }); + eventSource.addEventListener("token", (e) => { + token = JSON.parse(e.data); + }); + eventSource.addEventListener("error", (err: MessageEvent) => { eventSource.close(); if (err.data) { diff --git a/src/components/player/hooks/useSourceSelection.ts b/src/components/player/hooks/useSourceSelection.ts index d1a50792..826ed3d1 100644 --- a/src/components/player/hooks/useSourceSelection.ts +++ b/src/components/player/hooks/useSourceSelection.ts @@ -41,7 +41,7 @@ export function useEmbedScraping( try { if (providerApiUrl) { const baseUrlMaker = makeProviderUrl(providerApiUrl); - const conn = connectServerSideEvents( + const conn = await connectServerSideEvents( baseUrlMaker.scrapeEmbed(embedId, url), ["completed", "noOutput"] ); @@ -105,7 +105,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { try { if (providerApiUrl) { const baseUrlMaker = makeProviderUrl(providerApiUrl); - const conn = connectServerSideEvents( + const conn = await connectServerSideEvents( baseUrlMaker.scrapeSource(sourceId, scrapeMedia), ["completed", "noOutput"] ); @@ -146,7 +146,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { try { if (providerApiUrl) { const baseUrlMaker = makeProviderUrl(providerApiUrl); - const conn = connectServerSideEvents( + const conn = await connectServerSideEvents( baseUrlMaker.scrapeEmbed( result.embeds[0].embedId, result.embeds[0].url diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index c00d6d60..cf50b6a9 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -156,7 +156,7 @@ export function useScrape() { if (providerApiUrl) { startScrape(); const baseUrlMaker = makeProviderUrl(providerApiUrl); - const conn = connectServerSideEvents( + const conn = await connectServerSideEvents( baseUrlMaker.scrapeAll(media), ["completed", "noOutput"] ); diff --git a/src/index.tsx b/src/index.tsx index 8ef5a560..e943c462 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,6 +10,7 @@ import ReactDOM from "react-dom"; import { HelmetProvider } from "react-helmet-async"; import { useTranslation } from "react-i18next"; import { BrowserRouter, HashRouter } from "react-router-dom"; +import Turnstile from "react-turnstile"; import { useAsync } from "react-use"; import { Button } from "@/components/buttons/Button"; @@ -30,16 +31,12 @@ import { useLanguageStore } from "@/stores/language"; import { ProgressSyncer } from "@/stores/progress/ProgressSyncer"; import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer"; import { ThemeProvider } from "@/stores/theme"; +import { TurnstileProvider } from "@/stores/turnstile"; import { initializeChromecast } from "./setup/chromecast"; import { initializeOldStores } from "./stores/__old/migrations"; // initialize -const key = - (window as any)?.__CONFIG__?.VITE_KEY ?? import.meta.env.VITE_KEY ?? null; -if (key) { - (window as any).initMW(conf().PROXY_URLS, key); -} initializeChromecast(); function LoadingScreen(props: { type: "user" | "lazy" }) { @@ -148,6 +145,7 @@ function TheRouter(props: { children: ReactNode }) { ReactDOM.render( + }> diff --git a/src/setup/config.ts b/src/setup/config.ts index 47d18412..2f16714b 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -17,6 +17,7 @@ interface Config { NORMAL_ROUTER: boolean; BACKEND_URL: string; DISALLOWED_IDS: string; + TURNSTILE_KEY: string; } export interface RuntimeConfig { @@ -30,6 +31,7 @@ export interface RuntimeConfig { PROXY_URLS: string[]; BACKEND_URL: string; DISALLOWED_IDS: string[]; + TURNSTILE_KEY: string | null; } const env: Record = { @@ -43,6 +45,7 @@ const env: Record = { NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER, BACKEND_URL: import.meta.env.VITE_BACKEND_URL, DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS, + TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY, }; // loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js) @@ -63,6 +66,7 @@ function getKey(key: keyof Config, defaultString?: string): string { export function conf(): RuntimeConfig { const dmcaEmail = getKey("DMCA_EMAIL"); + const turnstileKey = getKey("TURNSTILE_KEY"); return { APP_VERSION, GITHUB_LINK, @@ -75,6 +79,7 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", + TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null, DISALLOWED_IDS: getKey("DISALLOWED_IDS", "") .split(",") .map((v) => v.trim()) diff --git a/src/stores/turnstile/index.tsx b/src/stores/turnstile/index.tsx new file mode 100644 index 00000000..907ffc2e --- /dev/null +++ b/src/stores/turnstile/index.tsx @@ -0,0 +1,81 @@ +import Turnstile, { BoundTurnstileObject } from "react-turnstile"; +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +import { conf } from "@/setup/config"; + +export interface TurnstileStore { + turnstile: BoundTurnstileObject | null; + cbs: ((token: string | null) => void)[]; + setTurnstile(v: BoundTurnstileObject | null): void; + getToken(): Promise; + processToken(token: string | null): void; +} + +export const useTurnstileStore = create( + immer((set, get) => ({ + turnstile: null, + cbs: [], + processToken(token) { + const cbs = get().cbs; + cbs.forEach((fn) => fn(token)); + set((s) => { + s.cbs = []; + }); + }, + getToken() { + return new Promise((resolve, reject) => { + set((s) => { + s.cbs = [ + ...s.cbs, + (token) => { + if (!token) reject(new Error("Failed to get token")); + else resolve(token); + }, + ]; + }); + }); + }, + setTurnstile(v) { + set((s) => { + s.turnstile = v; + }); + }, + })) +); + +export function getTurnstile() { + return useTurnstileStore.getState().turnstile; +} + +export function isTurnstileInitialized() { + return !!getTurnstile(); +} + +export function getTurnstileToken() { + const turnstile = getTurnstile(); + turnstile?.reset(); + turnstile?.execute(); + return useTurnstileStore.getState().getToken(); +} + +export function TurnstileProvider() { + const siteKey = conf().TURNSTILE_KEY; + const setTurnstile = useTurnstileStore((s) => s.setTurnstile); + const processToken = useTurnstileStore((s) => s.processToken); + if (!siteKey) return null; + return ( + { + setTurnstile(bound); + }} + onError={() => { + processToken(null); + }} + onVerify={(token) => { + processToken(token); + }} + /> + ); +}