您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Only show cards from followed users, not tags
// ==UserScript== // @name Chub.AI Timeline - Followed users only // @description Only show cards from followed users, not tags // @match https://chub.ai/* // @grant GM.setValue // @grant GM.getValue // @grant GM_registerMenuCommand // @version 2025.08.09a // @author anden3 // @license MIT // @require https://cdn.jsdelivr.net/npm/@violentmonkey/url#sha384-MW/Hes7CLT6ZD4zwzTUVdtXL/VaIDQ3uMFVuOx46Q0xILNG6vEueFrCaYNKw+YE3 // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @namespace https://greasyfork.org/users/1499640 // ==/UserScript== /* globals VM */ /* jshint esversion: 11 */ let API_KEY = "YOUR API KEY HERE"; const API_BASE = "https://inference.chub.ai/api"; const INTERNAL_API_BASE = "https://gateway.chub.ai"; (async function() { 'use strict'; // Acquire API key. First check localStorage, next cache, and last the hardcoded value. const localStorageAPIKey = localStorage.getItem("URQL_TOKEN"); if (localStorageAPIKey !== null) { API_KEY = localStorageAPIKey; await GM.setValue("API_KEY", API_KEY); } else { const possiblyCachedAPIKey = await GM.getValue("API_KEY", null) ?? null; if (possiblyCachedAPIKey !== null) { API_KEY = possiblyCachedAPIKey; } else if (API_KEY !== "YOUR API KEY HERE") { await GM.setValue("API_KEY", API_KEY); } else { throw new Error("No API key could be located, please log in."); } } const { onNavigate } = VM; async function queryApi(url, queryParams = {}, method = "GET") { const searchParams = new URLSearchParams(queryParams); console.log(`${url}?${searchParams}`); const response = await fetch(`${url}?${searchParams}`, { method, headers: { "Content-Type": "application/json", "CH-API-KEY": API_KEY, }, }); if (!response.ok) { throw new Error(`Response status: ${response.status}`); } const result = await response.json(); if (result.error !== undefined) { console.error(`API Query failed: ${result}`); throw new Error(result); } return result; } async function queryPublicApi(endpoint, queryParams = {}, method = "GET") { return await queryApi(`${API_BASE}/${endpoint}`, queryParams, method); } async function queryInternalApi(endpoint, queryParams = {}, method = "GET") { return await queryApi(`${INTERNAL_API_BASE}/${endpoint}`, queryParams, method); } async function getProfile() { return await queryInternalApi("api/account"); } async function fetchTimeline(page) { const cards = await queryInternalApi("api/timeline/v1", { page, count: false }); return cards.data.nodes.map(card => { const author = card.fullPath.split("/")[0]; return { ...card, author, }; }); } async function fetchFollows() { const profile = await getProfile(); if (profile.user_name === "You") { alert("The API key provided to this userscript (Chub.AI Timeline - Followed users only) is invalid.\nDisable this userscript if this alert is annoying."); throw new Error("API key failed to authenticate user, most likely invalid."); } let follows = { users: [], tags: [], }; let page = 1; while (true) { let followData = await queryInternalApi(`api/follows/${profile.user_name}`, { page: page }); if (followData.follows.length + followData.tag_follows.length === 0) { break; } follows.users.push(...followData.follows); follows.tags.push(...followData.tag_follows); page += 1; // Sleep await new Promise(r => setTimeout(r, 1000)); } return follows; } async function getVisibleCards() { // Wait for cards to exist. try { await waitForElement("#chara-list", 5000); } catch { return []; } return Array.from(document.querySelectorAll("#chara-list > a")) .map(node => { const path = node.attributes.href.value; const fork = node.querySelector("a:has(.anticon-fork)")?.attributes?.href?.value ?? null; return { node, path, fork, } }); } async function cacheIsStale(key, thresholdMs = 60 * 1000) { const lastCacheUpdate = await GM.getValue(key, null); if (lastCacheUpdate === null) { return true; } const msSinceUpdate = Math.max(new Date() - new Date(lastCacheUpdate), 0); return msSinceUpdate >= thresholdMs; } const DEFAULT_CACHE_OPTIONS = { staleThresholdMs: 60 * 1000, mappingFn: foo => foo, }; // Tries from cache if fresh, else fetches values. async function getCachedCollection( cacheKey, fetchFn, cacheOptions, lastUpdateKey = `last${cacheKey}Update`, ) { const cache_options = { ...DEFAULT_CACHE_OPTIONS, ...cacheOptions, }; const _cmd = GM_registerMenuCommand(`Update ${cacheKey}`, async _ev => { const values = cache_options.mappingFn(await fetchFn); await GM.setValue(cacheKey, values); await GM.setValue(lastUpdateKey, new Date().toISOString()); }, { title: `Update Chub.AI ${cacheKey} cache` }); const cachedValues = await GM.getValue(cacheKey, null) ?? null; if (cachedValues === null || await cacheIsStale(lastUpdateKey, cache_options.staleThresholdMs)) { const values = cache_options.mappingFn(await fetchFn()); await GM.setValue(cacheKey, values); await GM.setValue(lastUpdateKey, new Date().toISOString()); return values; } else { return cachedValues; } } async function getFollows() { let followsData = await getCachedCollection("follows", fetchFollows, { staleThresholdMs: 60 * 60 * 1000, }); return { users: new Set(followsData.users.map(u => u.username)), tags: new Set(followsData.tags.map(t => t.tagname.toLowerCase())), }; } async function isTimelineSelected() { await new Promise(r => setTimeout(r, 2000)); // Wait for labels to exist. try { await waitForElement(".ant-select-selection-item", 2000); } catch { return false; } const selectedCategory = document.querySelector(".ant-select-selection-item")?.title ?? ""; return selectedCategory === "Timeline"; } async function timelineLoaded(page) { const follows = await getFollows(); const cards = await fetchTimeline(page); const cardElements = await getVisibleCards(); for (let i in cards) { const followingUser = follows.users.has(cards[i].author); if (!followingUser) { cardElements[i].node.style.display = "none"; } } } async function handleNavigate() { console.log(window.location); if (window.location.pathname !== "/") { return; } const urlParams = new URLSearchParams(window.location.search); if ((urlParams.size === 0 && await isTimelineSelected()) || urlParams.get("segment") === "timeline") { timelineLoaded(parseInt(urlParams.get("page") ?? "1")); } } // Watch route change VM.onNavigate(handleNavigate); // Call it once for the initial state handleNavigate(); // Source: https://stackoverflow.com/a/61511955 function waitForElement(selector, timeout = -1) { return new Promise((resolve, reject) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } let timeoutPid; if (timeout >= 0) { timeoutPid = setTimeout(() => { observer.disconnect(); reject(`Timed out after ${timeout} ms.`); }, timeout); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { if (timeoutPid) { clearTimeout(timeoutPid); } observer.disconnect(); resolve(document.querySelector(selector)); } }); // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336 observer.observe(document.body, { childList: true, subtree: true }); }); } })();