您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Content filtering for Bluesky: block keywords, enforce alt-text, and auto-whitelist followed accounts. Now with improved dynamic content handling.
// ==UserScript== // @name Bluesky Content Manager // @namespace https://gf.qytechs.cn/en/users/567951-stuart-saddler // @version 3.4.9 // @description Content filtering for Bluesky: block keywords, enforce alt-text, and auto-whitelist followed accounts. Now with improved dynamic content handling. // @license MIT // @match https://bsky.app/* // @icon https://i.ibb.co/YySpmDk/Bluesky-Content-Manager.png // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @connect bsky.social // @run-at document-idle // ==/UserScript== (async function () { 'use strict'; /***** CONFIG & GLOBALS *****/ const filteredTerms = (JSON.parse(GM_getValue('filteredTerms','[]'))||[]) .map(t=>t.trim().toLowerCase()); const whitelistedUsers = new Set( (JSON.parse(GM_getValue('whitelistedUsers','[]'))||[]) .map(u=>normalizeUsername(u)) ); let altTextEnforcementEnabled = GM_getValue('altTextEnforcementEnabled', false); let blockedCount = 0, menuCommandId = null; /***** CSS INJECTION *****/ const CSS = ` .content-filtered { display: none !important; height: 0 !important; overflow: hidden !important; } .bluesky-filter-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; z-index: 1000000; box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 300px; max-width: 350px; font-family: Arial, sans-serif; color: #333; } .bluesky-filter-dialog h2 { margin-top: 0; color: #0079d3; font-size: 1.5em; font-weight: bold; } .bluesky-filter-dialog p { font-size: 0.9em; margin-bottom: 10px; color: #555; } .bluesky-filter-dialog textarea { width: calc(100% - 16px); height: 150px; padding: 8px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; background: #f9f9f9; color: #000; } .bluesky-filter-dialog label { display: block; margin-top: 10px; font-size: 0.9em; color: #333; } .bluesky-filter-dialog input[type="checkbox"] { margin-right: 6px; } .bluesky-filter-dialog .button-container { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } .bluesky-filter-dialog button { display: flex; align-items: center; justify-content: center; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 1em; text-align: center; } .bluesky-filter-dialog .save-btn { background-color: #0079d3; color: white; } .bluesky-filter-dialog .cancel-btn { background-color: #f2f2f2; color: #333; } .bluesky-filter-dialog button:hover { opacity: 0.9; } .bluesky-filter-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999999; } `; GM_addStyle(CSS); /***** UTILITIES *****/ function normalizeUsername(u){ return u.toLowerCase().replace(/[\u200B-\u200F\u202A-\u202F]/g,'').trim(); } function escapeRegExp(s){ return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); } function cleanText(t){ return t.normalize('NFKD').replace(/\s+/g,' ').toLowerCase().trim(); } function getPostContainer(node){ let cur=node; while(cur&&cur!==document.body){ if(cur.matches('[data-testid="post"], div[role="link"], article')) return cur; cur=cur.parentElement; } return null; } function shouldProcessPage(){ const p=window.location.pathname; return !p.startsWith('/profile/') && p!=='/notifications'; } function isContentImage(img){ return !( img.closest('[data-testid="avatar"]') || img.classList.contains('avatar') || img.classList.contains('css-9pa8cd') ); } /***** MENU & CONFIG UI *****/ function updateMenuCommand(){ if(menuCommandId) GM_unregisterMenuCommand(menuCommandId); menuCommandId = GM_registerMenuCommand(`Configure Filters (${blockedCount} blocked)`, showConfigUI); } function createConfigUI(){ const overlay=document.createElement('div'); overlay.className='bluesky-filter-overlay'; const dialog=document.createElement('div'); dialog.className='bluesky-filter-dialog'; dialog.innerHTML=` <h2>Bluesky Content Manager</h2> <p>Blocklist Keywords (one per line). Case-insensitive, plural forms match.</p> <textarea spellcheck="false">${filteredTerms.join('\n')}</textarea> <label> <input type="checkbox" ${altTextEnforcementEnabled?'checked':''}> Enable Alt-Text Enforcement </label> <div class="button-container"> <button class="cancel-btn">Cancel</button> <button class="save-btn">Save</button> </div> `; document.body.appendChild(overlay); document.body.appendChild(dialog); const close=()=>{ dialog.remove(); overlay.remove(); }; dialog.querySelector('.cancel-btn').addEventListener('click', close); overlay.addEventListener('click', close); dialog.querySelector('.save-btn').addEventListener('click', async ()=>{ const lines=dialog.querySelector('textarea').value.split('\n'); const terms=lines.map(l=>l.trim().toLowerCase()).filter(l=>l); await GM_setValue('filteredTerms', JSON.stringify(terms)); altTextEnforcementEnabled = dialog.querySelector('input[type="checkbox"]').checked; await GM_setValue('altTextEnforcementEnabled', altTextEnforcementEnabled); blockedCount=0; close(); location.reload(); }); } function showConfigUI(){ createConfigUI(); } /***** AUTH & PROFILE *****/ let sessionToken=null, currentUserDid=null; const profileCache = new Map(); function waitForAuth(){ return new Promise((res,rej)=>{ let attempts=0, max=30; (function check(){ attempts++; const s=localStorage.getItem('BSKY_STORAGE'); if(s){ try{ const p=JSON.parse(s); if(p.session?.accounts?.[0]?.accessJwt){ sessionToken=p.session.accounts[0].accessJwt; currentUserDid=p.session.accounts[0].did; return res(); } }catch{} } if(attempts>=max) return rej('Auth timeout'); setTimeout(check,1000); })(); }); } async function fetchProfile(did){ if(!sessionToken) return null; if(profileCache.has(did)) return profileCache.get(did); return new Promise((res,rej)=>{ GM_xmlhttpRequest({ method:'GET', url:`https://bsky.social/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`, headers:{ 'Authorization':`Bearer ${sessionToken}`, 'Accept':'application/json' }, onload(resp){ if(resp.status===200){ try{ const d=JSON.parse(resp.responseText); profileCache.set(did,d); return res(d); } catch(e){ return rej(e); } } if(resp.status===401){ sessionToken=null; return rej('Auth expired'); } rej(`HTTP ${resp.status}`); }, onerror(e){ rej(e); } }); }); } /***** AUTO-WHITELIST *****/ async function fetchAllFollows(cursor=null, acc=[]){ let url=`https://bsky.social/xrpc/app.bsky.graph.getFollows?actor=${encodeURIComponent(currentUserDid)}`; if(cursor) url+=`&cursor=${cursor}`; return new Promise((res,rej)=>{ GM_xmlhttpRequest({ method:'GET', url, headers:{ 'Authorization':`Bearer ${sessionToken}`, 'Accept':'application/json' }, onload(resp){ if(resp.status===200){ try{ const d=JSON.parse(resp.responseText); const all=acc.concat(d.follows||[]); if(d.cursor) return fetchAllFollows(d.cursor, all).then(res).catch(rej); return res(all); }catch(e){ return rej(e); } } rej(`HTTP ${resp.status}`); }, onerror(e){ rej(e); } }); }); } async function autoWhitelistFollowedAccounts(){ if(!sessionToken||!currentUserDid) return; try{ const f=await fetchAllFollows(); f.forEach(u=>{ let h=(u.subject?.handle)||u.handle; if(h&&!h.startsWith('@')) h='@'+h; whitelistedUsers.add(normalizeUsername(h)); }); }catch{} } /***** PROCESS POSTS *****/ async function processPost(post){ if(isWhitelisted(post)) return; if(!shouldProcessPage()) return; const container=getPostContainer(post); if(!container || container.classList.contains('bluesky-processed')) return; if(altTextEnforcementEnabled){ const imgs=Array.from(post.querySelectorAll('img')).filter(isContentImage); if(imgs.some(i=>i.alt.trim()==="")){ container.remove(); blockedCount++; updateMenuCommand(); return; } for(const img of imgs){ const txt=img.alt.trim()||img.getAttribute('aria-label')?.trim()||""; const c=cleanText(txt); if(filteredTerms.some(term=>{ const re=new RegExp(`\\b${escapeRegExp(term)}\\b`,'i'); return re.test(txt)||re.test(c); })){ container.remove(); blockedCount++; updateMenuCommand(); return; } } } const authorLink=post.querySelector('a[href^="/profile/"]'); if(authorLink){ const rn=authorLink.querySelector('span')?.textContent||authorLink.textContent; const cn=cleanText(rn); if(filteredTerms.some(term=>{ const re=new RegExp(`\\b${escapeRegExp(term)}\\b`,'i'); return re.test(rn.toLowerCase())||re.test(cn); })){ container.remove(); blockedCount++; updateMenuCommand(); return; } } const ptext=post.querySelector('div[data-testid="postText"]'); if(ptext){ const rt=ptext.textContent, ct=cleanText(rt); if(filteredTerms.some(term=>{ const re=new RegExp(`\\b${escapeRegExp(term)}\\b`,'i'); return re.test(rt.toLowerCase())||re.test(ct); })){ container.remove(); blockedCount++; updateMenuCommand(); return; } } const all=container.textContent||"", call=cleanText(all); if(filteredTerms.some(term=>{ const re=new RegExp(`\\b${escapeRegExp(term)}\\b`,'i'); return re.test(all.toLowerCase())||re.test(call); })){ container.remove(); blockedCount++; updateMenuCommand(); return; } } function isWhitelisted(post){ const link=post.querySelector('a[href^="/profile/"]'); if(!link) return false; const id=link.href.split('/profile/')[1].split(/[/?#]/)[0]; return whitelistedUsers.has(normalizeUsername('@'+id)); } /***** OBSERVE MUTATIONS *****/ function observePosts(){ const observer=new MutationObserver(muts=>{ if(!shouldProcessPage()) return; muts.forEach(m=>{ if(m.type==='childList'){ Array.from(m.addedNodes) .filter(n=>n.nodeType===1) .forEach(node=>{ if(node.tagName==='IMG'){ const c=getPostContainer(node); if(c) setTimeout(()=>processPost(c),100); } node.querySelectorAll('img').forEach(i=>{ const c=getPostContainer(i); if(c) setTimeout(()=>processPost(c),100); }); node.querySelectorAll('a[href^="/profile/"]').forEach(l=>{ const c=getPostContainer(l); if(c) setTimeout(()=>processPost(c),100); }); }); } else if(m.type==='attributes' && ['alt','aria-label','src'].includes(m.attributeName)){ const c=getPostContainer(m.target); if(c) setTimeout(()=>processPost(c),100); } else if(m.type==='characterData'){ const c=getPostContainer(m.target.parentElement); if(c) setTimeout(()=>processPost(c),100); } }); }); observer.observe(document.body,{ childList:true, subtree:true, attributes:true, attributeFilter:['alt','aria-label','src'], characterData:true }); let last=location.pathname; setInterval(()=>{ if(location.pathname!==last){ last=location.pathname; observer.disconnect(); if(shouldProcessPage()){ observer.observe(document.body,{ childList:true, subtree:true, attributes:true, attributeFilter:['alt','aria-label','src'], characterData:true }); } } },1000); document.addEventListener('load',e=>{ if(e.target.tagName==='IMG'){ const c=getPostContainer(e.target); if(c) setTimeout(()=>processPost(c),100); } },true); } /***** INIT *****/ document.querySelectorAll('[data-testid="post"], article, div[role="link"]') .forEach(el=>processPost(el)); updateMenuCommand(); if(shouldProcessPage()){ waitForAuth() .then(()=>{ autoWhitelistFollowedAccounts(); observePosts(); }) .catch(()=>{/* auth failed */}); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址