diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1483de6b..5cfd3305 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,6 +6,13 @@ "image": "mcr.microsoft.com/devcontainers/universal:2-linux", "features": { "ghcr.io/devcontainers/features/node:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "svelte.svelte-vscode" + ] + } } // Features to add to the dev container. More info: https://containers.dev/features. diff --git a/.gitignore b/.gitignore index ae56830a..0eda8388 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ desktop.ini # node node_modules +.pnpm-store # static build build diff --git a/api/src/core/api.js b/api/src/core/api.js index e4d3dfcf..e49348b8 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -154,41 +154,43 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { }); app.post('/', (req, res, next) => { - if (!env.sessionEnabled || req.rateLimitKey) { - return next(); - } + // if (!env.sessionEnabled || req.rateLimitKey) { + // return next(); + // } - try { - const authorization = req.header("Authorization"); - if (!authorization) { - return fail(res, "error.api.auth.jwt.missing"); - } + // try { + // const authorization = req.header("Authorization"); + // if (!authorization) { + // return fail(res, "error.api.auth.jwt.missing"); + // } - if (authorization.length >= 256) { - return fail(res, "error.api.auth.jwt.invalid"); - } + // if (authorization.length >= 256) { + // return fail(res, "error.api.auth.jwt.invalid"); + // } - const [ type, token, ...rest ] = authorization.split(" "); - if (!token || type.toLowerCase() !== 'bearer' || rest.length) { - return fail(res, "error.api.auth.jwt.invalid"); - } + // const [ type, token, ...rest ] = authorization.split(" "); + // if (!token || type.toLowerCase() !== 'bearer' || rest.length) { + // return fail(res, "error.api.auth.jwt.invalid"); + // } - if (!jwt.verify(token)) { - return fail(res, "error.api.auth.jwt.invalid"); - } + // if (!jwt.verify(token)) { + // return fail(res, "error.api.auth.jwt.invalid"); + // } - req.rateLimitKey = hashHmac(token, 'rate'); - } catch { - return fail(res, "error.api.generic"); - } + // req.rateLimitKey = hashHmac(token, 'rate'); + // } catch { + // return fail(res, "error.api.generic"); + // } next(); }); app.post('/', apiLimiter); - app.use('/', express.json({ limit: 1024 })); + app.use('/', express.json({ limit: 8192 })); - app.use('/', (err, _, res, next) => { + app.use('/', (err, req, res, next) => { if (err) { + // console.error('Failed to normalize request:', req); + console.error('error:', err); const { status, body } = createResponse("error", { code: "error.api.invalid_body", }); @@ -234,6 +236,8 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { const { success, data: normalizedRequest } = await normalizeRequest(request); if (!success) { + console.error('Failed to normalize request:', request); + console.log('data:', normalizedRequest); return fail(res, "error.api.invalid_body"); } @@ -250,15 +254,20 @@ export const runAPI = async (express, app, __dirname, isPrimary = true) => { return fail(res, `error.api.${parsed.error}`, context); } + if (!request.cookies) + request.cookies = {"X-nocookies" : "included"}; + try { const result = await match({ host: parsed.host, patternMatch: parsed.patternMatch, params: normalizedRequest, + inputCookies: request.cookies, }); res.status(result.status).json(result.body); - } catch { + } catch (e) { + console.error("Failed to match request", request, e); fail(res, "error.api.generic"); } }) diff --git a/api/src/processing/match.js b/api/src/processing/match.js index e2d6aa07..6d2a9550 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -29,14 +29,24 @@ import loom from "./services/loom.js"; import facebook from "./services/facebook.js"; import bluesky from "./services/bluesky.js"; import xiaohongshu from "./services/xiaohongshu.js"; +import Cookie from "./cookie/cookie.js"; let freebind; -export default async function({ host, patternMatch, params }) { +export default async function({ host, patternMatch, params, inputCookies }) { const { url } = params; assert(url instanceof URL); let dispatcher, requestIP; + console.log('Calling with cookies:', inputCookies); + + let cookies = new Cookie({}); + for (const [k, v] of Object.entries(inputCookies)) { + cookies.set(k, v); + } + + console.log('Calling with cookies:', cookies); + if (env.freebindCIDR) { if (!freebind) { freebind = await import('freebind'); @@ -133,6 +143,7 @@ export default async function({ host, patternMatch, params }) { isAudioOnly, h265: params.tiktokH265, alwaysProxy: params.alwaysProxy, + cookies: cookies, }); break; diff --git a/api/src/processing/request.js b/api/src/processing/request.js index d512bfe5..b0471328 100644 --- a/api/src/processing/request.js +++ b/api/src/processing/request.js @@ -77,9 +77,10 @@ export function createResponse(responseType, responseData) { } export function normalizeRequest(request) { - return apiSchema.safeParseAsync(request).catch(() => ( - { success: false } - )); + return apiSchema.safeParseAsync(request).catch((err) => { + console.log('Zod validation error:', JSON.stringify(err, null, 2)); + return { success: false }; + }); } export function getIP(req) { diff --git a/api/src/processing/schema.js b/api/src/processing/schema.js index 48d8b058..248051ec 100644 --- a/api/src/processing/schema.js +++ b/api/src/processing/schema.js @@ -47,5 +47,7 @@ export const apiSchema = z.object({ twitterGif: z.boolean().default(true), youtubeHLS: z.boolean().default(false), + + cookies: z.record(z.string()).default({}), }) .strict(); diff --git a/api/src/processing/services/tiktok.js b/api/src/processing/services/tiktok.js index 6fec01d8..1497ef99 100644 --- a/api/src/processing/services/tiktok.js +++ b/api/src/processing/services/tiktok.js @@ -8,9 +8,12 @@ import { createStream } from "../../stream/manage.js"; const shortDomain = "https://vt.tiktok.com/"; export default async function(obj) { - const cookie = new Cookie({}); + // const cookie = new Cookie({}); + const cookie = obj.cookies; let postId = obj.postId; + console.log("With TikTok using cookies", cookie); + if (!postId) { let html = await fetch(`${shortDomain}${obj.shortLink}`, { redirect: "manual", diff --git a/web/i18n/en/button.json b/web/i18n/en/button.json index 1ea7fb41..39a8eff7 100644 --- a/web/i18n/en/button.json +++ b/web/i18n/en/button.json @@ -1,5 +1,6 @@ { "gotit": "got it", + "signin": "sign into service", "cancel": "cancel", "reset": "reset", "done": "done", diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index 2788d9e4..a35d6ccc 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -59,7 +59,7 @@ "api.content.post.unavailable": "couldn't find anything about this post. its visibility may be limited or it may not exist. make sure your link works and try again in a few seconds!", "api.content.post.private": "couldn't get anything about this post because it's from a private account. try a different link!", - "api.content.post.age": "this post is age-restricted and isn't available without logging in. try a different link!", + "api.content.post.age": "this post is age-restricted and isn't available without logging in. Sign in to your account.", "api.youtube.no_matching_format": "youtube didn't return a valid video + audio format combo, either video or audio is missing. formats for this video may be re-encoding on youtube's side or something went wrong when parsing them. try enabling the hls option in video settings!", "api.youtube.decipher": "youtube updated its decipher algorithm and i couldn't extract the info about the video. try again in a few seconds, but if this issue sticks, please report it!", diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index c10db574..9f8a9ac2 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -38,7 +38,8 @@ let isDisabled = false; let isLoading = false; - $: isBotCheckOngoing = $turnstileEnabled && !$turnstileSolved; + // $: isBotCheckOngoing = $turnstileEnabled && !$turnstileSolved; + $: isBotCheckOngoing = false; const validLink = (url: string) => { try { @@ -132,7 +133,8 @@ --> {#if env.DEFAULT_API || (!$page.url.host.endsWith(".cobalt.tools") && $page.url.host !== "cobalt.tools")}
- {$t("save.label.community_instance")} + KDDResearch Instance +
{/if} @@ -155,13 +157,14 @@ autocapitalize="off" maxlength="512" placeholder={$t("save.input.placeholder")} - aria-label={isBotCheckOngoing - ? $t("a11y.save.link_area.turnstile") - : $t("a11y.save.link_area")} data-form-type="other" disabled={isDisabled} /> + + {#if $link && !isLoading} ($link = "")} /> {/if} diff --git a/web/src/components/save/buttons/DownloadButton.svelte b/web/src/components/save/buttons/DownloadButton.svelte index 981bb0ad..bca83858 100644 --- a/web/src/components/save/buttons/DownloadButton.svelte +++ b/web/src/components/save/buttons/DownloadButton.svelte @@ -77,6 +77,50 @@ if (response.status === "error") { changeDownloadButton("error"); + console.log(response.error.code); + + if (response.error.code == "error.api.content.post.age") { + return createDialog({ + id: "save-error", + type: "small", + meowbalt: "error", + buttons: [ + { + text: $t("button.signin"), + main: true, + action: () => { + // opens a new tab for the user and redirects them to the login page + + const website = URL.parse(link); + + if (!website) + throw new Error("Invalid URL"); + + console.log(website); + + switch (website.hostname) { + case "www.tiktok.com": + website.pathname = "/login/qrcode"; + break; + default: + website.pathname = "/login"; + break; + } + + window.open(website.href, "_blank"); + }, + }, + { + text: $t("button.cancel"), + main: false, + action: () => {}, + }, + ], + bodyText: $t(response.error.code, response?.error?.context), + }); + } + + return createDialog({ ...defaultErrorPopup, bodyText: $t(response.error.code, response?.error?.context), diff --git a/web/src/lib/state/turnstile.ts b/web/src/lib/state/turnstile.ts index 9024247d..f427d7db 100644 --- a/web/src/lib/state/turnstile.ts +++ b/web/src/lib/state/turnstile.ts @@ -8,10 +8,11 @@ export const turnstileCreated = writable(false); export const turnstileEnabled = derived( [settings, cachedInfo], ([$settings, $cachedInfo]) => { - return !!$cachedInfo?.info?.cobalt?.turnstileSitekey && - !( - $settings.processing.enableCustomApiKey && - $settings.processing.customApiKey.length > 0 - ) + // return !!$cachedInfo?.info?.cobalt?.turnstileSitekey && + // !( + // $settings.processing.enableCustomApiKey && + // $settings.processing.customApiKey.length > 0 + // ) + return false; } )