From 0dfca92cdffae5d3c07c399dc1e4c21b365837ce Mon Sep 17 00:00:00 2001 From: ashley Date: Sun, 16 Nov 2025 20:46:42 +0100 Subject: [PATCH] move telemetry to somewhere else --- src/libpoketube/init/pages-api.js | 942 +++++++----------------------- 1 file changed, 208 insertions(+), 734 deletions(-) diff --git a/src/libpoketube/init/pages-api.js b/src/libpoketube/init/pages-api.js index e2ad05f6..731f9812 100644 --- a/src/libpoketube/init/pages-api.js +++ b/src/libpoketube/init/pages-api.js @@ -41,508 +41,6 @@ module.exports = function (app, config, renderTemplate) { f.body.pipe(res); }); - const telemetryConfig = { telemetry: true } - - const path = require("path") - -const statsFile = path.join(__dirname, "stats.json") - -if (!fs.existsSync(statsFile)) { - fs.writeFileSync( - statsFile, - JSON.stringify({ videos: {}, browsers: {}, os: {}, users: {} }, null, 2) - ) -} - -function parseUA(ua) { - let browser = "unknown" - let os = "unknown" - - if (/firefox/i.test(ua)) browser = "firefox" - else if (/chrome|chromium|crios/i.test(ua)) browser = "chrome" - else if (/safari/i.test(ua)) browser = "safari" - else if (/edge/i.test(ua)) browser = "edge" - - if (/windows/i.test(ua)) os = "windows" - else if (/android/i.test(ua)) os = "android" - else if (/mac os|macintosh/i.test(ua)) os = "macos" - else if (/linux/i.test(ua)) os = "gnu-linux" - else if (/iphone|ipad|ios/i.test(ua)) os = "ios" - - return { browser, os } -} - - app.post("/api/stats", (req, res) => { - if (!telemetryConfig.telemetry) return res.status(200).json({ ok: true }) - - const { videoId, userId } = req.body - if (!videoId) return res.status(400).json({ error: "missing videoId" }) - if (!userId) return res.status(400).json({ error: "missing userId" }) - - const ua = req.headers["user-agent"] || "" - const { browser, os } = parseUA(ua) - - const raw = fs.readFileSync(statsFile, "utf8") - const data = JSON.parse(raw) -if (!data.users) data.users = {} - - if (!data.videos[videoId]) data.videos[videoId] = 0 - data.videos[videoId]++ - - if (!data.browsers[browser]) data.browsers[browser] = 0 - data.browsers[browser]++ - - if (!data.os[os]) data.os[os] = 0 - data.os[os]++ - - if (!data.users[userId]) data.users[userId] = true - - fs.writeFileSync(statsFile, JSON.stringify(data, null, 2)) - res.json({ ok: true }) -}) -app.get("/api/stats/optout", (req, res) => { - res.send(` - - - - Poke – Opt out of stats - - - - - -
-

Stats opt-out

-

- This page lets you turn off anonymous usage stats for this browser. - Poke will remember this choice using localStorage only (no cookies). -

- -

- Anonymous stats help us understand which videos are popular and which platforms people use — - without collecting personal data. You can read the full details here: - Privacy Policy. -

- - Opt out of anonymous stats -
- -

- • To see the stats UI (if enabled on this instance), visit - /api/stats?view=human.
- • For raw JSON, use /api/stats?view=json. -

-
- - - -`) -}) - -app.get("/api/stats", (req, res) => { - const view = (req.query.view || "").toString() - - // JSON view – explicit: /api/stats?view=json - if (view === "json") { - if (!telemetryConfig.telemetry) { - return res.json({ videos: {}, browsers: {}, os: {}, totalUsers: 0 }) - } - - const raw = fs.readFileSync(statsFile, "utf8") - const data = JSON.parse(raw) - - if (!data.videos) data.videos = {} - if (!data.browsers) data.browsers = {} - if (!data.os) data.os = {} - if (!data.users) data.users = {} - - const sortedVideos = Object.entries(data.videos) - .sort((a, b) => b[1] - a[1]) - .slice(0, 10) - - const topVideos = Object.fromEntries(sortedVideos) - - return res.json({ - videos: topVideos, - browsers: data.browsers, - os: data.os, - totalUsers: Object.keys(data.users).length - }) - } - - // Human view – /api/stats?view=human (just stats UI) - if (view === "human") { - const telemetryOn = telemetryConfig.telemetry - - return res.send(` - - - - Improving Poke – Stats - - - - - -
-

Anonymous stats

-

- These stats are aggregated locally on this Poke instance. For what is collected (and what is not), - see privacy policy. -

- -

Current anonymous stats

-

Loading…

- - -

Top videos (local-only)

-

Up to 10 most watched videos on this instance.

- - -
- -

API usage

-

- • Human view (this page): /api/stats?view=human
- • JSON view (for scripts/tools): /api/stats?view=json
- • Opt out for this browser: /api/stats/optout -

-
- - - -`) - } - - // any other view value (including "/api/stats" with no ?view) -> landing page HTML - return res.send(` - - - - Improving Poke - - - - - -
- Poke logo -

Improving Poke

-

Private by design

- -

- At Poke, we do not collect or share any personal information. - That's our privacy promise in a nutshell. - To improve Poke we use a completely anonymous, local-only way to figure out how the site is being used. -

- -

- Any anonymous stats recorded by this instance come from the /api/stats system. - You can read exactly what is measured (and what is not) in our privacy policy: - here. -

- -
- -

API usage

-

- • Human view (stats UI): /api/stats?view=human
- • JSON view (for scripts/tools): /api/stats?view=json
- • Opt out for this browser: /api/stats/optout -

-
- -`) -}) - - app.get("/avatars/:v", async function (req, res) { var url = `https://yt3.ggpht.com/${req.params.v}`; @@ -556,35 +54,35 @@ app.get("/api/stats", (req, res) => { }); -app.get("/api/geo", async (req, res) => { - try { - let ip = - req.headers["x-forwarded-for"]?.split(",")[0].trim() || - req.socket.remoteAddress; + app.get("/api/geo", async (req, res) => { + try { + let ip = + req.headers["x-forwarded-for"]?.split(",")[0].trim() || + req.socket.remoteAddress; - if (ip && ip.startsWith("::ffff:")) { - ip = ip.slice(7); + if (ip && ip.startsWith("::ffff:")) { + ip = ip.slice(7); + } + + if (!ip) { + return res.status(400).json({ error: "No IP found" }); + } + + const response = await fetch(`https://ip2c.org/${ip}`); + const text = await response.text(); + const parts = text.trim().split(";"); + + const countryCode = parts[1] || "ZZ"; + + res.setHeader("Content-Type", "application/json"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.json({ countryCode }); + } catch (err) { + res.setHeader("Content-Type", "application/json"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.status(500).json({ countryCode: "ZZ", error: true, details: String(err) }); } - - if (!ip) { - return res.status(400).json({ error: "No IP found" }); - } - - const response = await fetch(`https://ip2c.org/${ip}`); - const text = await response.text(); - const parts = text.trim().split(";"); - - const countryCode = parts[1] || "ZZ"; - - res.setHeader("Content-Type", "application/json"); - res.setHeader("Access-Control-Allow-Origin", "*"); - res.json({ countryCode }); - } catch (err) { - res.setHeader("Content-Type", "application/json"); - res.setHeader("Access-Control-Allow-Origin", "*"); - res.status(500).json({ countryCode: "ZZ", error: true, details: String(err) }); - } -}); + }); @@ -610,44 +108,41 @@ app.get("/api/geo", async (req, res) => { f.body.pipe(res); }); -app.get("/api/nominatim/search", async (req, res) => { - try { - const url = new URL("https://nominatim.openstreetmap.org/search"); - // Forward all query params (format, q, limit, etc.) - for (const [key, value] of Object.entries(req.query)) { - url.searchParams.set(key, value); + app.get("/api/nominatim/search", async (req, res) => { + try { + const url = new URL("https://nominatim.openstreetmap.org/search"); + for (const [key, value] of Object.entries(req.query)) { + url.searchParams.set(key, value); + } + if (!url.searchParams.has("format")) url.searchParams.set("format", "json"); + + const r = await fetch(url.toString(), { + headers: { "Accept-Language": req.headers["accept-language"] || "en" } + }); + const data = await r.json(); + res.json(data); + } catch (err) { + res.status(500).json({ error: "Failed to fetch from nominatim" }); } - // Force JSON output if not specified - if (!url.searchParams.has("format")) url.searchParams.set("format", "json"); + }); - const r = await fetch(url.toString(), { - headers: { "Accept-Language": req.headers["accept-language"] || "en" } - }); - const data = await r.json(); - res.json(data); - } catch (err) { - res.status(500).json({ error: "Failed to fetch from nominatim" }); - } -}); + app.get("/api/nominatim/reverse", async (req, res) => { + try { + const url = new URL("https://nominatim.openstreetmap.org/reverse"); + for (const [key, value] of Object.entries(req.query)) { + url.searchParams.set(key, value); + } + if (!url.searchParams.has("format")) url.searchParams.set("format", "json"); -// Proxy for reverse geocoding -app.get("/api/nominatim/reverse", async (req, res) => { - try { - const url = new URL("https://nominatim.openstreetmap.org/reverse"); - for (const [key, value] of Object.entries(req.query)) { - url.searchParams.set(key, value); + const r = await fetch(url.toString(), { + headers: { "Accept-Language": req.headers["accept-language"] || "en" } + }); + const data = await r.json(); + res.json(data); + } catch (err) { + res.status(500).json({ error: "Failed to fetch from nominatim" }); } - if (!url.searchParams.has("format")) url.searchParams.set("format", "json"); - - const r = await fetch(url.toString(), { - headers: { "Accept-Language": req.headers["accept-language"] || "en" } - }); - const data = await r.json(); - res.json(data); - } catch (err) { - res.status(500).json({ error: "Failed to fetch from nominatim" }); - } -}); + }); app.get("/avatars/ytc/:v", async function (req, res) { var url = `https://yt3.googleusercontent.com/ytc/${req.params.v.replace("ytc", "")}`; @@ -680,39 +175,30 @@ app.get("/api/nominatim/reverse", async (req, res) => { try { let url = `${config.videourl}/companion/api/v1/captions/${id}?label=${l}`; - /* -let f = await fetch(url, { - headers: headers, - }); - - const body = await f.text(); - - res.send(body); - */ res.send("j"); } catch {} }); -app.get("/api/weather", async (req, res) => { - try { - const url = new URL("https://api.open-meteo.com/v1/forecast"); - for (const [key, value] of Object.entries(req.query)) { - url.searchParams.set(key, value); - } + app.get("/api/weather", async (req, res) => { + try { + const url = new URL("https://api.open-meteo.com/v1/forecast"); + for (const [key, value] of Object.entries(req.query)) { + url.searchParams.set(key, value); + } - const response = await fetch(url.toString()); - if (!response.ok) { - return res.status(response.status).json({ error: "Upstream error" }); - } + const response = await fetch(url.toString()); + if (!response.ok) { + return res.status(response.status).json({ error: "Upstream error" }); + } - const data = await response.json(); - res.json(data); - } catch (err) { - console.error(err); - res.status(500).json({ error: "Proxy error" }); - } -}); + const data = await response.json(); + res.json(data); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Proxy error" }); + } + }); app.get("/api/getEngagementData", async (req, res) => { const { fetch } = await import("undici"); @@ -742,23 +228,15 @@ app.get("/api/weather", async (req, res) => { const dislikePercentage = total > 0 ? ((dislikes / total) * 100).toFixed(2) : 0; const getLikePercentageColor = (percentage) => { - if (percentage >= 80) { - return "green"; - } else if (percentage >= 50) { - return "orange"; - } else { - return "red"; - } + if (percentage >= 80) return "green"; + else if (percentage >= 50) return "orange"; + else return "red"; }; const getDislikePercentageColor = (percentage) => { - if (percentage >= 50) { - return "red"; - } else if (percentage >= 20) { - return "orange"; - } else { - return "green"; - } + if (percentage >= 50) return "red"; + else if (percentage >= 20) return "orange"; + else return "green"; }; const likeColor = getLikePercentageColor(likePercentage); @@ -770,24 +248,19 @@ app.get("/api/weather", async (req, res) => { ).toFixed(2); const getUserScoreLabel = (score) => { - if (score >= 98) { - return "Masterpiece Video"; - } else if (score >= 80) { - return "Overwhelmingly Positive"; - } else if (score >= 60) { - return "Positive"; - } else if (score >= 40) { - return "Mixed"; - } else if (score >= 20) { - return "Negative"; - } else { - return "Overwhelmingly Negative"; - } + if (score >= 98) return "Masterpiece Video"; + else if (score >= 80) return "Overwhelmingly Positive"; + else if (score >= 60) return "Positive"; + else if (score >= 40) return "Mixed"; + else if (score >= 20) return "Negative"; + else return "Overwhelmingly Negative"; }; const userScoreLabel = getUserScoreLabel(userScore); const userScoreColor = - userScore >= 80 ? "green" : userScore >= 50 ? "orange" : "red"; + userScore >= 80 ? "green" : + userScore >= 50 ? "orange" : + "red"; const respon = { like_count: likes, @@ -825,7 +298,7 @@ app.get("/api/weather", async (req, res) => { let f = await modules.fetch(url, { method: req.method, - headers: headers, // Add headers to the fetch request + headers: headers, }); f.body.pipe(res); @@ -861,138 +334,139 @@ app.get("/api/weather", async (req, res) => { app.get("/api/v1", async (req, res) => { res.redirect("https://invid-api.poketube.fun/api/v1/stats"); }); -app.get("/api/version.json", async (req, res) => { - let latestCommitHash = null; - function getLatestCommitHash() { - try { - const out = execSync("git rev-parse --short HEAD", { stdio: ["ignore", "pipe", "pipe"] }) - .toString() - .trim(); - return out || null; - } catch { - return null; - } - } + app.get("/api/version.json", async (req, res) => { + let latestCommitHash = null; - function readOsRelease() { - try { - const text = fs.readFileSync("/etc/os-release", "utf8"); - const map = {}; - for (const line of text.split("\n")) { - const m = line.match(/^([A-Z0-9_]+)=(.*)$/); - if (!m) continue; - let v = m[2]; - if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) { - v = v.slice(1, -1); - } - map[m[1]] = v; + function getLatestCommitHash() { + try { + const out = execSync("git rev-parse --short HEAD", { stdio: ["ignore", "pipe", "pipe"] }) + .toString() + .trim(); + return out || null; + } catch { + return null; } - return { - id: map.ID || null, - id_like: map.ID_LIKE || null, - version_id: map.VERSION_ID || null, - name: map.NAME || null, - pretty_name: map.PRETTY_NAME || null, - }; - } catch { - return null; } - } - const osr = readOsRelease(); + function readOsRelease() { + try { + const text = fs.readFileSync("/etc/os-release", "utf8"); + const map = {}; + for (const line of text.split("\n")) { + const m = line.match(/^([A-Z0-9_]+)=(.*)$/); + if (!m) continue; + let v = m[2]; + if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) { + v = v.slice(1, -1); + } + map[m[1]] = v; + } + return { + id: map.ID || null, + id_like: map.ID_LIKE || null, + version_id: map.VERSION_ID || null, + name: map.NAME || null, + pretty_name: map.PRETTY_NAME || null, + }; + } catch { + return null; + } + } - const cpus = os.cpus() || []; - const totalMemoryGB = os.totalmem() / (1024 ** 3); - const freeMemoryGB = os.freemem() / (1024 ** 3); + const osr = readOsRelease(); - const roundedTotalGB = totalMemoryGB.toFixed(2); - const roundedFreeGB = freeMemoryGB.toFixed(2); + const cpus = os.cpus() || []; + const totalMemoryGB = os.totalmem() / (1024 ** 3); + const freeMemoryGB = os.freemem() / (1024 ** 3); - const loadavg = os.loadavg(); // [1m, 5m, 15m] - const uptimeSeconds = Math.floor(os.uptime()); + const roundedTotalGB = totalMemoryGB.toFixed(2); + const roundedFreeGB = freeMemoryGB.toFixed(2); - let platform = os.platform(); // 'linux', 'darwin', 'win32', etc. - if (platform === 'linux') platform = 'gnu/linux'; + const loadavg = os.loadavg(); + const uptimeSeconds = Math.floor(os.uptime()); - const kernelRelease = os.release(); // e.g., '6.8.0-40-generic' - const arch = os.arch(); // e.g., 'x64', 'arm64' - const hostname = os.hostname(); - const cpuModel = cpus[0]?.model || "Unknown CPU"; - const cpuCount = cpus.length; + let platform = os.platform(); + if (platform === 'linux') platform = 'gnu/linux'; - latestCommitHash = getLatestCommitHash(); + const kernelRelease = os.release(); + const arch = os.arch(); + const hostname = os.hostname(); + const cpuModel = cpus[0]?.model || "Unknown CPU"; + const cpuCount = cpus.length; - let invidious = null; - try { - const invTxt = await modules - .fetch("https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1/stats", { headers }) - .then(r => r.text()); - invidious = getJson(invTxt); - } catch { - invidious = null; - } + latestCommitHash = getLatestCommitHash(); - const { useragent, ...configWithoutUA } = cnf; + let invidious = null; + try { + const invTxt = await modules + .fetch("https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1/stats", { headers }) + .then(r => r.text()); + invidious = getJson(invTxt); + } catch { + invidious = null; + } - const response = { - pt_version: { - version: versmol, - version_full: verfull, - commit: latestCommitHash, - }, - branch, - updatequote, - relaseunixdate, - vernum: versionnumber, - codename, - config: configWithoutUA, - system: { - os_name: osr?.pretty_name || osr?.name || (platform === "linux" ? "GNU/Linux" : os.type()), - distro: osr ? { - pretty_name: osr.pretty_name, - name: osr.name, - id: osr.id, - id_like: osr.id_like, - version_id: osr.version_id, - } : null, - platform, - kernel_release: kernelRelease, - arch, - hostname, - ram_total: `${roundedTotalGB} GB`, - ram_free: `${roundedFreeGB} GB`, - cpu: cpuModel, - cpu_cores: cpuCount, - loadavg: { - "1m": Number(loadavg[0]?.toFixed(2) || 0), - "5m": Number(loadavg[1]?.toFixed(2) || 0), - "15m": Number(loadavg[2]?.toFixed(2) || 0), + const { useragent, ...configWithoutUA } = cnf; + + const response = { + pt_version: { + version: versmol, + version_full: verfull, + commit: latestCommitHash, }, - uptime_seconds: uptimeSeconds, - }, - packages: { - libpt: version, - node: process.version, - v8: process.versions.v8, - }, - invidious, - innertube, - flac: { - poketube_flac: "1.2a", - apple_musickit: "1.2.3", - poketube_normalize_volume: "1.2.23-yt", - }, - process: process.versions, - dependencies: pkg.dependencies, - poketubeapicode: (() => { - const invVer = invidious?.software?.version || "0"; - return btoa(String(Date.now()) + String(invVer)); - })(), - }; + branch, + updatequote, + relaseunixdate, + vernum: versionnumber, + codename, + config: configWithoutUA, + system: { + os_name: osr?.pretty_name || osr?.name || (platform === "linux" ? "GNU/Linux" : os.type()), + distro: osr ? { + pretty_name: osr.pretty_name, + name: osr.name, + id: osr.id, + id_like: osr.id_like, + version_id: osr.version_id, + } : null, + platform, + kernel_release: kernelRelease, + arch, + hostname, + ram_total: `${roundedTotalGB} GB`, + ram_free: `${roundedFreeGB} GB`, + cpu: cpuModel, + cpu_cores: cpuCount, + loadavg: { + "1m": Number(loadavg[0]?.toFixed(2) || 0), + "5m": Number(loadavg[1]?.toFixed(2) || 0), + "15m": Number(loadavg[2]?.toFixed(2) || 0), + }, + uptime_seconds: uptimeSeconds, + }, + packages: { + libpt: version, + node: process.version, + v8: process.versions.v8, + }, + invidious, + innertube, + flac: { + poketube_flac: "1.2a", + apple_musickit: "1.2.3", + poketube_normalize_volume: "1.2.23-yt", + }, + process: process.versions, + dependencies: pkg.dependencies, + poketubeapicode: (() => { + const invVer = invidious?.software?.version || "0"; + return btoa(String(Date.now()) + String(invVer)); + })(), + }; - res.json(response); -}); + res.json(response); + }); app.get("/api/instances.json", async (req, res) => {