diff --git a/api/src/processing/match.js b/api/src/processing/match.js index ffb92c23..b0022d08 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -127,7 +127,7 @@ export default async function({ host, patternMatch, params }) { case "tiktok": r = await tiktok({ postId: patternMatch.postId, - id: patternMatch.id, + shortLink: patternMatch.shortLink, fullAudio: params.tiktokFullAudio, isAudioOnly, h265: params.tiktokH265, diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 8d8bf4ac..a2136ad0 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -111,10 +111,10 @@ export const services = { tiktok: { patterns: [ ":user/video/:postId", - ":id", - "t/:id", + ":shortLink", + "t/:shortLink", ":user/photo/:postId", - "v/:id.html" + "v/:postId.html" ], subdomains: ["vt", "vm", "m"], }, diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index 2105a563..7f8982b5 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -39,7 +39,7 @@ export const testers = { pattern.id?.length === 6, "tiktok": pattern => - pattern.postId?.length <= 21 || pattern.id?.length <= 13, + pattern.postId?.length <= 21 || pattern.shortLink?.length <= 13, "tumblr": pattern => pattern.id?.length < 21 diff --git a/api/src/processing/services/tiktok.js b/api/src/processing/services/tiktok.js index 3c70e033..e205ce54 100644 --- a/api/src/processing/services/tiktok.js +++ b/api/src/processing/services/tiktok.js @@ -12,7 +12,7 @@ export default async function(obj) { let postId = obj.postId; if (!postId) { - let html = await fetch(`${shortDomain}${obj.id}`, { + let html = await fetch(`${shortDomain}${obj.shortLink}`, { redirect: "manual", headers: { "user-agent": genericUserAgent.split(' Chrome/1')[0] @@ -24,7 +24,7 @@ export default async function(obj) { if (html.startsWith(' { if (data.ips) { formatted[key].ips = data.ips.map(addr => { if (ip.isValid(addr)) { - return [ ip.parse(addr), 32 ]; + const parsed = ip.parse(addr); + const range = parsed.kind() === 'ipv6' ? 128 : 32; + return [ parsed, range ]; } return ip.parseCIDR(addr); diff --git a/docs/api.md b/docs/api.md index fc09a441..39b17209 100644 --- a/docs/api.md +++ b/docs/api.md @@ -3,7 +3,42 @@ this document provides info about methods and acceptable variables for all cobal > if you are looking for the documentation for the old (7.x) api, you can find > it [here](https://github.com/imputnet/cobalt/blob/7/docs/api.md) - + +## authentication +an api instance may be configured to require you to authenticate yourself. +if this is the case, you will typically receive an [error response](#error-response) +with a **`api.auth..missing`** code, which tells you that a particular method +of authentication is required. + +authentication is done by passing the `Authorization` header, containing +the authentication scheme and the token: +``` +Authorization: +``` + +currently, cobalt supports two ways of authentication. an instance can +choose to configure both, or neither: +- [`Api-Key`](#api-key-authentication) +- [`Bearer`](#bearer-authentication) + +### api-key authentication +the api key authentication is the most straightforward. the instance owner +will assign you an api key which you can then use to authenticate like so: +``` +Authorization: Api-Key aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee +``` + +if you are an instance owner and wish to configure api key authentication, +see the [instance](run-an-instance.md#api-key-file-format) documentation! + +### bearer authentication +the cobalt server may be configured to issue JWT bearers, which are short-lived +tokens intended for use by regular users (e.g. after passing a challenge). +currently, cobalt can issue tokens for successfully solved [turnstile](run-an-instance.md#list-of-all-environment-variables) +challenge, if the instance has turnstile configured. the resulting token is passed like so: +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` ## POST: `/` cobalt's main processing endpoint. @@ -108,3 +143,18 @@ response body type: `application/json` | `commit` | `string` | commit hash | | `branch` | `string` | git branch | | `remote` | `string` | git remote | + +## POST: `/session` + +used for generating JWT tokens, if enabled. currently, cobalt only supports +generating tokens when a [turnstile](run-an-instance.md#list-of-all-environment-variables) challenge solution +is submitted by the client. + +the turnstile challenge response is submitted via the `cf-turnstile-response` header. +### response body +| key | type | description | +|:----------------|:-----------|:-------------------------------------------------------| +| `token` | `string` | a `Bearer` token used for later request authentication | +| `exp` | `number` | number in seconds indicating the token lifetime | + +on failure, an [error response](#error-response) is returned. diff --git a/docs/examples/cookies.example.json b/docs/examples/cookies.example.json index 73f3378d..7996adeb 100644 --- a/docs/examples/cookies.example.json +++ b/docs/examples/cookies.example.json @@ -10,5 +10,8 @@ ], "twitter": [ "auth_token=; ct0=" + ], + "youtube_oauth": [ + "" ] } diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 08654d9f..272fbd35 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -72,11 +72,17 @@ sudo service nscd start | `RATELIMIT_MAX` | `20` | `30` | max requests per time window. requests above this amount will be blocked for the rate limit window duration. | | `DURATION_LIMIT` | `10800` | `18000` | max allowed video duration in **seconds**. | | `TUNNEL_LIFESPAN` | `90` | `120` | the duration for which tunnel info is stored in ram, **in seconds**. | +| `TURNSTILE_SITEKEY` | ➖ | `1x00000000000000000000BB` | [cloudflare turnstile](https://www.cloudflare.com/products/turnstile/) sitekey used by browser clients to request a challenge.\*\* | +| `TURNSTILE_SECRET` | ➖ | `1x0000000000000000000000000000000AA` | [cloudflare turnstile](https://www.cloudflare.com/products/turnstile/) secret used by cobalt to verify the client successfully solved the challenge.\*\* | +| `JWT_SECRET` | ➖ | ➖ | the secret used for issuing JWT tokens for request authentication. to choose a value, generate a random, secure, long string (ideally >=16 characters).\*\* | +| `JWT_EXPIRY` | `120` | `240` | the duration of how long a cobalt-issued JWT token will remain valid, in seconds. | | `API_KEY_URL` | ➖ | `file://keys.json` | the location of the api key database. for loading API keys, cobalt supports HTTP(S) urls, or local files by specifying a local path using the `file://` protocol. see the "api key file format" below for more details. | | `API_AUTH_REQUIRED` | ➖ | `1` | when set to `1`, the user always needs to be authenticated in some way before they can access the API (either via an api key or via turnstile, if enabled). | \* the higher the nice value, the lower the priority. [read more here](https://en.wikipedia.org/wiki/Nice_(Unix)). +\*\* in order to enable turnstile bot protection, all three **`TURNSTILE_SITEKEY`, `TURNSTILE_SECRET` and `JWT_SECRET`** need to be set. + #### FREEBIND_CIDR setting a `FREEBIND_CIDR` allows cobalt to pick a random IP for every download and use it for all requests it makes for that particular download. to use freebind in cobalt, you need to follow its [setup instructions](https://github.com/imputnet/freebind.js?tab=readme-ov-file#setup) first. if you configure this option while running cobalt diff --git a/web/i18n/en/about.json b/web/i18n/en/about.json index cfe9129f..10d31ec2 100644 --- a/web/i18n/en/about.json +++ b/web/i18n/en/about.json @@ -8,12 +8,6 @@ "page.terms": "terms and ethics", "page.credits": "thanks & licenses", - "community.discord": "community discord server", - "community.twitter": "news account on twitter", - "community.github": "github repo", - "community.email": "support email", - "community.telegram": "news channel on telegram", - "heading.general": "general terms", "heading.licenses": "licenses", "heading.summary": "best way to save what you love", @@ -27,5 +21,14 @@ "heading.responsibility": "user responsibilities", "heading.abuse": "reporting abuse", "heading.motivation": "motivation", - "heading.testers": "beta testers" + "heading.testers": "beta testers", + + "support.github": "check out cobalt's source code, contribute changes, or report issues", + "support.discord": "chat with the community and developers about cobalt or ask for help", + "support.twitter": "follow cobalt's updates and development on your twitter timeline", + "support.telegram": "stay up to date with latest cobalt updates via a telegram channel", + + "support.description.issue": "if you want to report a bug or some other recurring issue, please do it on github.", + "support.description.help": "use discord for any other questions. describe the issue properly in #cobalt-support or else no one will be able help you.", + "support.description.best-effort": "all support is best effort and not guaranteed, a reply might take some time." } diff --git a/web/i18n/en/about/credits.md b/web/i18n/en/about/credits.md index 6c001f58..812f3394 100644 --- a/web/i18n/en/about/credits.md +++ b/web/i18n/en/about/credits.md @@ -6,6 +6,17 @@ import BetaTesters from "$components/misc/BetaTesters.svelte"; +
+ + +cobalt is made with love and care by the [imput](https://imput.net/) research and development team. + +you can support us on the [donate page](/donate)! +
+
+ import { t } from "$lib/i18n/translations"; + import { openURL } from "$lib/download"; + + import IconExternalLink from "@tabler/icons-svelte/IconExternalLink.svelte"; + + import IconBrandGithub from "@tabler/icons-svelte/IconBrandGithub.svelte"; + import IconBrandTwitter from "@tabler/icons-svelte/IconBrandTwitter.svelte"; + import IconBrandDiscord from "@tabler/icons-svelte/IconBrandDiscord.svelte"; + import IconBrandTelegram from "@tabler/icons-svelte/IconBrandTelegram.svelte"; + + const platformIcons = { + github: { + icon: IconBrandGithub, + color: "#8842cd", + }, + discord: { + icon: IconBrandDiscord, + color: "#5865f2", + }, + twitter: { + icon: IconBrandTwitter, + color: "#1da1f2", + }, + telegram: { + icon: IconBrandTelegram, + color: "#1c9efb", + }, + }; + + export let platform: keyof typeof platformIcons; + export let externalLink: string; + + + + + diff --git a/web/src/components/buttons/Switcher.svelte b/web/src/components/buttons/Switcher.svelte index a72c4e77..2ead0caf 100644 --- a/web/src/components/buttons/Switcher.svelte +++ b/web/src/components/buttons/Switcher.svelte @@ -47,6 +47,7 @@ background: var(--button); box-shadow: var(--button-box-shadow); padding: var(--switcher-padding); + gap: calc(var(--switcher-padding) - 1.5px); } .switcher :global(.button.active) { diff --git a/web/src/components/dialog/DialogButton.svelte b/web/src/components/dialog/DialogButton.svelte index f977cc6a..69aaf722 100644 --- a/web/src/components/dialog/DialogButton.svelte +++ b/web/src/components/dialog/DialogButton.svelte @@ -23,21 +23,36 @@ onDestroy(() => clearInterval(interval)); } - - - +{#if button.link} + + {button.text} + +{:else} + +{/if} diff --git a/web/src/components/icons/Clipboard.svelte b/web/src/components/icons/Clipboard.svelte index 5ceb9618..cf2d3b93 100644 --- a/web/src/components/icons/Clipboard.svelte +++ b/web/src/components/icons/Clipboard.svelte @@ -1,11 +1,11 @@ - - - + + + - - - - - + + + + + diff --git a/web/src/components/icons/Music.svelte b/web/src/components/icons/Music.svelte index de73841f..f9b07373 100644 --- a/web/src/components/icons/Music.svelte +++ b/web/src/components/icons/Music.svelte @@ -1,5 +1,5 @@ - - - + + + diff --git a/web/src/components/icons/Mute.svelte b/web/src/components/icons/Mute.svelte index a869fb09..9e8d2d5b 100644 --- a/web/src/components/icons/Mute.svelte +++ b/web/src/components/icons/Mute.svelte @@ -1,5 +1,5 @@ - - + + diff --git a/web/src/components/icons/Sparkles.svelte b/web/src/components/icons/Sparkles.svelte index 91d8d2eb..8d061f45 100644 --- a/web/src/components/icons/Sparkles.svelte +++ b/web/src/components/icons/Sparkles.svelte @@ -1,5 +1,5 @@ - - - + + + diff --git a/web/src/components/misc/SectionHeading.svelte b/web/src/components/misc/SectionHeading.svelte index 4645f2d0..e3b1e635 100644 --- a/web/src/components/misc/SectionHeading.svelte +++ b/web/src/components/misc/SectionHeading.svelte @@ -8,6 +8,9 @@ export let title: string; export let sectionId: string; export let beta = false; + export let copyData = ""; + + const sectionURL = `${$page.url.origin}${$page.url.pathname}#${sectionId}`; let copied = false; @@ -19,19 +22,27 @@
-

{title}

+

+ {title} +

+ {#if beta} -
{$t("general.beta")}
+
+ {$t("general.beta")} +
{/if} +
diff --git a/web/src/components/save/Omnibox.svelte b/web/src/components/save/Omnibox.svelte index a900def7..eed0ad71 100644 --- a/web/src/components/save/Omnibox.svelte +++ b/web/src/components/save/Omnibox.svelte @@ -225,7 +225,7 @@ flex-direction: column; max-width: 640px; width: 100%; - gap: 10px; + gap: 8px; } #input-container { diff --git a/web/src/lib/api/safety-warning.ts b/web/src/lib/api/safety-warning.ts index 175ddd67..26ad6aa4 100644 --- a/web/src/lib/api/safety-warning.ts +++ b/web/src/lib/api/safety-warning.ts @@ -98,7 +98,7 @@ export const customInstanceWarning = async () => { text: get(t)("button.continue"), color: "red", main: true, - timeout: 15000, + timeout: 5000, action: () => { _actions.resolve(); updateSetting({ diff --git a/web/src/lib/types/dialog.ts b/web/src/lib/types/dialog.ts index 69e27b5d..c0721f97 100644 --- a/web/src/lib/types/dialog.ts +++ b/web/src/lib/types/dialog.ts @@ -6,7 +6,8 @@ export type DialogButton = { color?: "red", main: boolean, timeout?: number, // milliseconds - action: () => unknown | Promise + action: () => unknown | Promise, + link?: string } export type SmallDialogIcons = "warn-red"; diff --git a/web/src/routes/about/community/+page.svelte b/web/src/routes/about/community/+page.svelte index e0638015..f5c20bf4 100644 --- a/web/src/routes/about/community/+page.svelte +++ b/web/src/routes/about/community/+page.svelte @@ -4,61 +4,76 @@ import { contacts } from "$lib/env"; import { t } from "$lib/i18n/translations"; - import DonateAltItem from "$components/donate/DonateAltItem.svelte"; + import AboutSupport from "$components/about/AboutSupport.svelte"; + + let buttonContainerWidth: number; - -
- {#if $locale !== "ru"} - +
+ - - {/if} + {#if $locale === "ru"} + + {:else} + + + {/if} +
- +
+ {$t("about.support.description.issue")} - + {#if $locale !== "ru"} + {$t("about.support.description.help")} + {/if} - {#if $locale === "ru"} - - {/if} + {$t("about.support.description.best-effort")} +
diff --git a/web/src/routes/settings/audio/+page.svelte b/web/src/routes/settings/audio/+page.svelte index eb0ac679..06555a42 100644 --- a/web/src/routes/settings/audio/+page.svelte +++ b/web/src/routes/settings/audio/+page.svelte @@ -24,8 +24,8 @@ - diff --git a/web/src/routes/settings/debug/+page.svelte b/web/src/routes/settings/debug/+page.svelte index 71079369..edb612f9 100644 --- a/web/src/routes/settings/debug/+page.svelte +++ b/web/src/routes/settings/debug/+page.svelte @@ -1,12 +1,20 @@ {#if $settings.advanced.debug} -
-

device:

-
- {JSON.stringify(device, null, 2)} -
- -

app:

-
- {JSON.stringify(app, null, 2)} -
- -

settings:

-
- {JSON.stringify($storedSettings, null, 2)} -
- -

version:

-
- {JSON.stringify($version, null, 2)} -
+
+ {#each sections as { title, data }, i} +
+ +
+ {JSON.stringify(data, null, 2)} +
+
+ {/each}
{/if}