diff --git a/package.json b/package.json
index 81ba899d..508a65a2 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"esbuild": "^0.14.51",
"express": "^4.18.1",
"express-rate-limit": "^6.3.0",
+ "fetch-socks": "^1.2.0",
"ffmpeg-static": "^5.1.0",
"hls-parser": "^0.10.7",
"nanoid": "^4.0.2",
diff --git a/src/cobalt.js b/src/cobalt.js
index 949cccba..8cd69210 100644
--- a/src/cobalt.js
+++ b/src/cobalt.js
@@ -1,7 +1,8 @@
import "dotenv/config";
import express from "express";
-
+import { setGlobalDispatcher } from "undici";
+import { socksDispatcher } from "fetch-socks";
import { Bright, Green, Red } from "./modules/sub/consoleText.js";
import { getCurrentBranch, shortCommit } from "./modules/sub/currentCommit.js";
import { loadLoc } from "./localization/manager.js";
@@ -21,8 +22,31 @@ app.disable('x-powered-by');
await loadLoc();
-const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port));
-const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port));
+const torEnabled = (process.env.torHost && process.env.torPort && true) ? true : false;
+const torGlobal = (process.env.torGlobal && process.env.torGlobal == "true") ? true : false;
+global.torEnabled = torEnabled;
+
+if (torEnabled) {
+ let torProxy = {
+ type: 5,
+ host: process.env.torHost,
+ port: Number(process.env.torPort)
+ }
+ let torOptions = {
+ connect: {
+ timeout: 30000
+ }
+ }
+ let twitterTorOptions = torOptions;
+ twitterTorOptions['connect']['rejectUnauthorized'] = false;
+
+ global.torDispatcher = socksDispatcher(torProxy, torOptions)
+ global.twitterTorDispatcher = socksDispatcher(torProxy, twitterTorOptions)
+ if (torGlobal) setGlobalDispatcher(global.torDispatcher)
+}
+
+const apiMode = (process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port))) ? true : false;
+const webMode = (process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port))) ? true : false;
if (apiMode) {
const { runAPI } = await import('./core/api.js');
diff --git a/src/modules/api.js b/src/modules/api.js
index 92fa5374..7e2fdc19 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -14,7 +14,11 @@ export async function getJSON(originalURL, lang, obj) {
hostname = new URL(url).hostname.split('.'),
host = hostname[hostname.length - 2];
+ if (url.startsWith('http://')) url = url.replace('http://', 'https://');
if (!url.startsWith('https://')) return apiJSON(0, { t: errorUnsupported(lang) });
+ if(url.startsWith("https://www.")) {
+ url = url.replace("https://www.", "https://")
+ }
let overrides = hostOverrides(host, url);
host = overrides.host;
diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js
index b572a8b7..c8bebc5e 100644
--- a/src/modules/pageRender/page.js
+++ b/src/modules/pageRender/page.js
@@ -571,7 +571,7 @@ export default function(obj) {
-
+
diff --git a/src/modules/processing/hostOverrides.js b/src/modules/processing/hostOverrides.js
index 88553e35..754d1cbd 100644
--- a/src/modules/processing/hostOverrides.js
+++ b/src/modules/processing/hostOverrides.js
@@ -3,30 +3,10 @@ export default function (inHost, inURL) {
let url = String(inURL);
switch(host) {
- case "youtube":
- if (url.startsWith("https://youtube.com/live/") || url.startsWith("https://www.youtube.com/live/")) {
- url = url.split("?")[0].replace("www.", "");
- url = `https://youtube.com/watch?v=${url.replace("https://youtube.com/live/", "")}`
- }
- if (url.includes('youtube.com/shorts/')) {
- url = url.split('?')[0].replace('shorts/', 'watch?v=');
- }
- break;
- case "youtu":
- if (url.startsWith("https://youtu.be/")) {
- host = "youtube";
- url = `https://youtube.com/watch?v=${url.replace("https://youtu.be/", "")}`
- }
- break;
- case "vxtwitter":
- case "x":
- if (url.startsWith("https://x.com/")) {
- host = "twitter";
- url = url.replace("https://x.com/", "https://twitter.com/")
- }
- if (url.startsWith("https://vxtwitter.com/")) {
- host = "twitter";
- url = url.replace("https://vxtwitter.com/", "https://twitter.com/")
+ case "reddittorjg6rue252oqsxryoxengawnmo46qy4kyii5wtqnwfj4ooad":
+ if (url.startsWith("https://reddittorjg6rue252oqsxryoxengawnmo46qy4kyii5wtqnwfj4ooad.onion/")) {
+ host = "reddit";
+ url = url.replace("https://reddittorjg6rue252oqsxryoxengawnmo46qy4kyii5wtqnwfj4ooad.onion/", "https://reddit.com/")
}
break;
case "tumblr":
@@ -36,10 +16,72 @@ export default function (inHost, inURL) {
}
break;
case "twitch":
- if (url.includes('clips.twitch.tv')) {
+ if (url.startsWith("https://clips.twitch.tv")) {
url = url.split('?')[0].replace('clips.twitch.tv/', 'twitch.tv/_/clip/');
}
break;
+ case "vxtwitter":
+ case "fixvx":
+ case "fxtwitter":
+ case "twittpr":
+ case "fixupx":
+ case "x":
+ case "twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid":
+ if (url.startsWith("https://vxtwitter.com/")) {
+ host = "twitter";
+ url = url.replace("https://vxtwitter.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://fixvx.com/")) {
+ host = "twitter";
+ url = url.replace("https://fixvx.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://fxtwitter.com/")) {
+ host = "twitter";
+ url = url.replace("https://fxtwitter.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://d.fxtwitter.com/")) {
+ host = "twitter";
+ url = url.replace("https://d.fxtwitter.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://twittpr.com/")) {
+ host = "twitter";
+ url = url.replace("https://twittpr.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://d.twittpr.com/")) {
+ host = "twitter";
+ url = url.replace("https://d.twittpr.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://fixupx.com/")) {
+ host = "twitter";
+ url = url.replace("https://fixupx.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://d.fixupx.com/")) {
+ host = "twitter";
+ url = url.replace("https://d.fixupx.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://x.com/")) {
+ host = "twitter";
+ url = url.replace("https://x.com/", "https://twitter.com/")
+ }
+ if (url.startsWith("https://twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/")) {
+ host = "twitter";
+ url = url.replace("https://twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/", "https://twitter.com/")
+ }
+ break;
+ case "youtube":
+ if (url.startsWith("https://youtube.com/live/")) {
+ url = `https://youtube.com/watch?v=${url.split("?")[0].replace("https://youtube.com/live/", "")}`
+ }
+ if (url.startsWith("https://youtube.com/shorts/")) {
+ url = url.split('?')[0].replace('shorts/', 'watch?v=');
+ }
+ break;
+ case "youtu":
+ if (url.startsWith("https://youtu.be/")) {
+ host = "youtube";
+ url = `https://youtube.com/watch?v=${url.replace("https://youtu.be/", "")}`
+ }
+ break;
}
return {
host: host,
diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js
index eedf8ec1..21c95d01 100644
--- a/src/modules/processing/matchActionDecider.js
+++ b/src/modules/processing/matchActionDecider.js
@@ -44,6 +44,10 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
params = { type: r.type };
break;
case "reddit":
+ // ffmpeg doesn't support SOCKS5 proxies, which is necessary to use tor.
+ // ffmpeg won't be able to resolve the .onion address, so we need to
+ // change it back to the clearnet counterpart
+ r.urls.forEach((url, index) => { r.urls[index] = url.replace('redditdotzhmh3mao6r5i2j7speppwqkizwo7vksy3mbz5iz7rlhocyd.onion', 'redd.it') });
responseType = r.typeId;
params = { type: r.type };
break;
diff --git a/src/modules/processing/services/reddit.js b/src/modules/processing/services/reddit.js
index c985831a..4d2fd9dc 100644
--- a/src/modules/processing/services/reddit.js
+++ b/src/modules/processing/services/reddit.js
@@ -1,3 +1,4 @@
+import { fetch, getGlobalDispatcher } from "undici";
import { genericUserAgent, maxVideoDuration } from "../../config.js";
import { getCookie, updateCookieValues } from "../cookie/manager.js";
@@ -21,7 +22,7 @@ async function getAccessToken() {
|| Number(values.expiry) > new Date().getTime();
if (!needRefresh) return values.access_token;
- const data = await fetch('https://www.reddit.com/api/v1/access_token', {
+ const data = await fetch(`https://www.${redditURL}/api/v1/access_token`, {
method: 'POST',
headers: {
'authorization': `Basic ${Buffer.from(
@@ -48,13 +49,24 @@ async function getAccessToken() {
}
export default async function(obj) {
- const url = new URL(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.title}.json`);
+ let redditURL;
+ let regularURL = "reddit.com";
+ let torURL = "reddittorjg6rue252oqsxryoxengawnmo46qy4kyii5wtqnwfj4ooad.onion";
+ if (torEnabled) redditURL = torURL
+ else redditURL = regularURL;
+
+ let twitterDispatcher;
+ twitterDispatcher = false;
+ let redditDispatcher = global.torDispatcher ? global.torDispatcher : getGlobalDispatcher();
+
+ const url = new URL(`https://www.${redditURL}/r/${obj.sub}/comments/${obj.id}/${obj.title}.json`);
+ console.log(url);
const accessToken = await getAccessToken();
- if (accessToken) url.hostname = 'oauth.reddit.com';
+ if (accessToken) url.hostname = `oauth.${redditURL}`;
let data = await fetch(
- url, { headers: accessToken && { authorization: `Bearer ${accessToken}` } }
+ url, { dispatcher: redditDispatcher, headers: accessToken && { authorization: `Bearer ${accessToken}` } }
).then((r) => { return r.json() }).catch(() => { return false });
if (!data) return { error: 'ErrorCouldntFetch' };
@@ -69,12 +81,12 @@ export default async function(obj) {
video = data["secure_media"]["reddit_video"]["fallback_url"].split('?')[0],
audioFileLink = video.match('.mp4') ? `${video.split('_')[0]}_audio.mp4` : `${data["secure_media"]["reddit_video"]["fallback_url"].split('DASH')[0]}audio`;
- await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
+ await fetch(audioFileLink, { dispatcher: redditDispatcher, method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
// fallback for videos with variable audio quality
if (!audio) {
audioFileLink = `${video.split('_')[0]}_AUDIO_128.mp4`
- await fetch(audioFileLink, { method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
+ await fetch(audioFileLink, { dispatcher: redditDispatcher, method: "HEAD" }).then((r) => { if (Number(r.status) === 200) audio = true }).catch(() => { audio = false });
}
let id = video.split('/')[3];
diff --git a/src/modules/processing/services/twitter.js b/src/modules/processing/services/twitter.js
index a2510413..40364473 100644
--- a/src/modules/processing/services/twitter.js
+++ b/src/modules/processing/services/twitter.js
@@ -1,10 +1,22 @@
+import { fetch, getGlobalDispatcher } from "undici";
import { genericUserAgent } from "../../config.js";
+import { socksDispatcher } from "fetch-socks";
function bestQuality(arr) {
return arr.filter(v => v["content_type"] === "video/mp4").sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"]
}
export default async function(obj) {
+ let twitterURL;
+ let regularURL = "twitter.com";
+ let torURL = "twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion";
+ if (torEnabled) twitterURL = torURL
+ else twitterURL = regularURL;
+
+ let twitterDispatcher;
+ twitterDispatcher = false;
+ twitterDispatcher = global.twitterTorDispatcher ? global.twitterTorDispatcher : getGlobalDispatcher();
+
let _headers = {
"user-agent": genericUserAgent,
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
@@ -14,17 +26,20 @@ export default async function(obj) {
"accept-language": "en"
};
+ // guest accounts cannot be created through the onion website (rate limited)
let activateURL = `https://api.twitter.com/1.1/guest/activate.json`;
- let graphqlTweetURL = `https://twitter.com/i/api/graphql/0hWvDhmW8YQ-S_ib3azIrw/TweetResultByRestId`;
- let graphqlSpaceURL = `https://twitter.com/i/api/graphql/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById`;
+
+ let graphqlTweetURL = `https://${twitterURL}/i/api/graphql/0hWvDhmW8YQ-S_ib3azIrw/TweetResultByRestId`;
+ let graphqlSpaceURL = `https://${twitterURL}/i/api/graphql/Gdz2uCtmIGMmhjhHG3V7nA/AudioSpaceById`;
let req_act = await fetch(activateURL, {
+ dispatcher: twitterDispatcher,
method: "POST",
headers: _headers
- }).then((r) => { return r.status === 200 ? r.json() : false }).catch(() => { return false });
+ }).then((r) => { return r.status === 200 ? r.json() : false }).catch((err) => { return false });
if (!req_act) return { error: 'ErrorCouldntFetch' };
- _headers["host"] = "twitter.com";
+ _headers["host"] = twitterURL;
_headers["content-type"] = "application/json";
_headers["x-guest-token"] = req_act["guest_token"];
@@ -39,7 +54,7 @@ export default async function(obj) {
query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1);
query = `${graphqlTweetURL}?variables=${query.variables}&features=${query.features}`;
- let TweetResultByRestId = await fetch(query, { headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
+ let TweetResultByRestId = await fetch(query, { dispatcher: twitterDispatcher, headers: _headers }).then((r) => { return r.status === 200 ? r.json() : false }).catch((e) => { return false });
// {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}}
if (!TweetResultByRestId || TweetResultByRestId.data.tweetResult.result.__typename !== "Tweet") return { error: 'ErrorTweetUnavailable' };
@@ -79,7 +94,7 @@ export default async function(obj) {
}
// spaces no longer work with guest authorization
if (obj.spaceId) {
- _headers["host"] = "twitter.com";
+ _headers["host"] = twitterURL;
_headers["content-type"] = "application/json";
let query = {
@@ -90,14 +105,14 @@ export default async function(obj) {
query.features = new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1);
query = `${graphqlSpaceURL}?variables=${query.variables}&features=${query.features}`;
- let AudioSpaceById = await fetch(query, { headers: _headers }).then((r) => {return r.status === 200 ? r.json() : false}).catch((e) => { return false });
+ let AudioSpaceById = await fetch(query, { dispatcher: twitterDispatcher, headers: _headers }).then((r) => {return r.status === 200 ? r.json() : false}).catch((e) => { return false });
if (!AudioSpaceById) return { error: 'ErrorEmptyDownload' };
if (!AudioSpaceById.data.audioSpace.metadata) return { error: 'ErrorEmptyDownload' };
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay !== true) return { error: 'TwitterSpaceWasntRecorded' };
let streamStatus = await fetch(
- `https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }
+ `https://${twitterURL}/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { dispatcher: twitterDispatcher, headers: _headers }
).then((r) =>{ return r.status === 200 ? r.json() : false }).catch(() => { return false });
if (!streamStatus) return { error: 'ErrorCouldntFetch' };
diff --git a/src/modules/setup.js b/src/modules/setup.js
index cb5aa184..94f5fc91 100644
--- a/src/modules/setup.js
+++ b/src/modules/setup.js
@@ -44,6 +44,7 @@ function setup() {
ob['apiURL'] = `http://localhost:9000/`;
ob['apiPort'] = 9000;
if (apiURL && apiURL !== "localhost") ob['apiURL'] = `https://${apiURL.toLowerCase()}/`;
+ if (apiURL && apiURL.endsWith('.onion/')) ob['apiURL'] = apiURL.replace('https://', 'http://');
console.log(Bright("\nGreat! Now, what port will it be running on? (9000)"));