PincongFilter (20190506完成基本功能)
不正经的简介:
你有说话放屁的自由,我就有掩耳捂鼻的权力。
正经的简介:
Depend on Firefox at least 52esr and Greasemonkey.
过滤来自特定id的问题、文章、回答及评论,对匿名用户无效。
同时对于未被过滤的非匿名用户,在其用户名或头像旁显示该用户的id。
目前只能通过修改代码中特定变量的方法添加所需过滤的id。
本来打算放在pastebin的,想想还是算了……
不接受来自浏览器兼容性的bug report。
20190430更新:
过滤文章的评论。
修复额外的显示用户id的行为。
20190506更新:
显示过滤计数。
过滤列表修改/导入/导出。
P.S. 但是无法过滤文章的评论,因为这些评论的用户信息都没有data-id。这个要看网站后台的心情了。 已修复
P.S. 当然网站后台想要封杀这个脚本也非常容易。这个也看网站后台的心情了。
你有说话放屁的自由,我就有掩耳捂鼻的权力。
正经的简介:
Depend on Firefox at least 52esr and Greasemonkey.
过滤来自特定id的问题、文章、回答及评论,对匿名用户无效。
同时对于未被过滤的非匿名用户,在其用户名或头像旁显示该用户的id。
本来打算放在pastebin的,想想还是算了……
不接受来自浏览器兼容性的bug report。
20190430更新:
过滤文章的评论。
修复额外的显示用户id的行为。
20190506更新:
显示过滤计数。
过滤列表修改/导入/导出。
P.S. 当然网站后台想要封杀这个脚本也非常容易。这个也看网站后台的心情了。
// ==UserScript==
// @name PincongFilter
// @namespace PincongFilter
// @match https://pincong.rocks/*
// @version 1
// @grant none
// @run-at document-start
// ==/UserScript==
(function(){
let doc=document;
let html=doc.documentElement;
let map=function(f,i){for(let n of i)f(n);}
let newtag=function(t){return doc.createElement(t);}
let newtext=function(s){return doc.createTextNode(s);}
let addev=function(n,e,f){return n.addEventListener(e,f);}
let selid=function(i){return doc.getElementById(i);}
let seltag=function(t){return doc.getElementsByTagName(t);}
let selcss=function(c,r){return (r?r:doc).querySelectorAll(c);}
let selcss1=function(c,r){return (r?r:doc).querySelector(c);}
let hasattr=function(n,a){return n.hasAttribute(a);}
let getattr=function(n,a){return n.getAttribute(a);}
let setattr=function(n,a,v){n.setAttribute(a,v);}
let delattr=function(n,a){if(hasattr(n,a))n.removeAttribute(a);}
let appendnode=function(r,n){n?r.appendChild(n):null;}
let delnode=function(n){n?n.remove():null;}
let watch=function(n,p,f){
let config={};
let leastprops=['childList','attributes','characterData'];
let validconfig=false;
let watcher=new MutationObserver(f);
map(n=>config[n]=true,p);
for(let prop of leastprops)
validconfig=validconfig||config[prop];
if(!validconfig)config[leastprops[0]]=true;
watcher.observe(n,config);
addev(window,'beforeunload',()=>watcher.disconnect());
}
let hiddenstart=function(r,f){
r.hidden=true;
addev(doc,'DOMContentLoaded',()=>{f();r.hidden=false;});
}
let clicknode=function(n){
let e=document.createEvent('MouseEvents');
e.initEvent('click',true,true);
n.dispatchEvent(e);
};
let cmpstr=function(a,b){
let lencmp=a.length-b.length;
if(lencmp===0)
return a<b?-1:1;
return lencmp;
}
let synclist=function(src,dst){
dst.length=0;
for(let item of src){
if(dst.includes(item))continue;
if(item.length===0)continue;
dst.push(item)
}
dst.sort(cmpstr);
}
// question or article
let question_selector='div.aw-common-list>div.aw-item';
// answer for question or comment for article
let answer_selector='div.aw-item[id^="answer_list_"]';
// comment for answer or comment
let comment_selector='div.aw-comment-box>div.aw-comment-list>ul>li';
// prefix of user path
let user_prefix='/people/';
// blacklist of userid as string
let blacklist=[];
// static blacklist of userid, not visiable in UI
let staticlist=[];
let counter=0;
let filter_answer=function(node){
let usernode=selcss1('div.mod-head a.aw-user-name[data-id]',node);
// only do on non-anonymous user
if(usernode){
let userid=getattr(usernode,'data-id');
if(staticlist.includes(userid)||blacklist.includes(userid)){
console.debug('filter answer:',usernode.textContent);
delnode(node);
counter+=1;
showcounter();
return;
}
usernode.textContent+=' ('+userid+')';
}
watch(node,['subtree'],commentfilter);
}
let filter_question=function(node){
let usernode=selcss1('a.aw-user-name[data-id]:first-child',node);
// anonymous user, do nothing
if(!usernode)return;
let userid=getattr(usernode,'data-id');
if(staticlist.includes(userid)||blacklist.includes(userid)){
console.debug('filter question:',userid);
delnode(node);
counter+=1;
showcounter();
return;
}
// anonymous user, do nothing
if(userid.length===0)return;
let p=newtag('p');
appendnode(p,newtext(userid));
appendnode(usernode,p);
}
let filter_comment=function(node){
let usernode=selcss1('a.aw-user-name.author[data-id]:not([data--])',node);
// anonymous user, do nothing
if(!usernode)return;
let userid=getattr(usernode,'data-id');
if(staticlist.includes(userid)||blacklist.includes(userid)){
console.debug('filter comment:',usernode.textContent);
delnode(node);
counter+=1;
showcounter();
return;
}
usernode.textContent+=' ('+userid+')';
setattr(usernode,'data--','');
}
let commentfilter=function(){
map(filter_comment,selcss(comment_selector))
}
let loadblacklist=function(list){
try{
let _str=localStorage.getItem('PincongFilter');
if(_str===null)_str=JSON.stringify({'filter':[]});
let _data=JSON.parse(_str);
synclist(_data['filter'],list);
}catch(e){
console.error('error:',e);
}
}
let saveblacklist=function(list){
try{
let _data={'filter':[]}
let _str=selid('PincongFilter-blacklist').value;
synclist(_str.split('\n'),_data['filter']);
localStorage.setItem('PincongFilter',JSON.stringify(_data));
loadblacklist(list);
}catch(e){
console.error('error:',e);
}
}
let impblacklist=function(ev){
let finput=newtag('input');
finput.type='file';
finput.accept='.json';
finput.multiple=false;
finput.oninput=function(iev){
if(iev.target.files.length<1)return;
let reader=new FileReader();
reader.onload=function(rev){
try{
let _data={'filter':[]};
let json=JSON.parse(rev.target.result);
let area=selid('PincongFilter-blacklist');
synclist(json['filter'],_data['filter']);
area.value=_data['filter'].join('\n');
saveblacklist(blacklist);
}catch(e){
console.error('error:',e)
}
};
for(let file of iev.target.files)
reader.readAsText(file);
}
clicknode(finput);
}
let expblacklist=function(ev){
let data=new Blob([localStorage.getItem('PincongFilter')],
{type:'application/json'})
let url=URL.createObjectURL(data);
let a=newtag('a');
a.href=url;
a.download='PincongFilter.json';
clicknode(a);
}
let changeblacklist=function(ev){
saveblacklist(blacklist);
}
let createui=function(){
let parent=selcss1('nav>ul.nav');
if(!parent)return;
let root=newtag('li');
let a=newtag('a');
let con=newtag('span');
let div=newtag('div');
let area=newtag('textarea');
let btndiv=newtag('div')
let impbtn=newtag('button');
let expbtn=newtag('button');
appendnode(parent,root);
appendnode(root,a);
appendnode(root,div);
appendnode(a,newtext('PincongFilter'));
appendnode(a,con);
appendnode(div,area);
appendnode(div,btndiv);
appendnode(btndiv,impbtn);
appendnode(btndiv,expbtn);
appendnode(impbtn,newtext('Import'));
appendnode(expbtn,newtext('Export'));
a.href='javascript:;';
con.id='PincongFilter-counter';
div.classList.add('aw-dropdown','pull-right','hidden-xs');
area.id='PincongFilter-blacklist';
area.placeholder='One user id per line.';
area.rows=Math.max(blacklist.length+1,1);
if(blacklist.length)
area.cols=blacklist[blacklist.length-1].length;
area.value=blacklist.join('\n');
addev(area,'input',changeblacklist);
btndiv.style.cssFloat='right';
addev(impbtn,'click',impblacklist);
addev(expbtn,'click',expblacklist);
}
let showcounter=function(){
if(counter){
let span=selid('PincongFilter-counter');
span.textContent=[' (',counter,')'].join('');
}
}
let mainfilter=function(){
loadblacklist(blacklist);
createui();
map(filter_answer,selcss(answer_selector));
map(filter_question,selcss(question_selector));
}
hiddenstart(html,mainfilter);
})()
25 个评论

不错不错,有前途
在这里的这个身份不会直接出现在其他地方。
大手子炫技大赛开始了?
一直没搞懂怎么在TorBrowser里加脚本,能否提供一个简易教程?
试了一下,加到bookmark了,但点了毫无反应啊,能否在telegra.ph写个教程?
测试了一下,题主的脚本能显示用户的数字id,但屏蔽功能不好用呢。
亲测可用,比@looose 的略方便
就爱你们这样顽皮的小孩 @komorebi @looose
因催斯听
原来大手子的脚本就是靠看用户ID来判断是否是连续注册的小号
以前还有点踩来判断匿名,可惜现在这个漏洞被堵上了
这样看来还是转世党比较安全呢
大手子要不画一个品葱ID活跃的时间线?
原来大手子的脚本就是靠看用户ID来判断是否是连续注册的小号
以前还有点踩来判断匿名,可惜现在这个漏洞被堵上了
这样看来还是转世党比较安全呢
大手子要不画一个品葱ID活跃的时间线?
我们品葱大手子的思路和编程能力比国营的@looose 还是要强很多滴
大手子我为你骄傲!!
大手子我为你骄傲!!
@looose 站长 你的@情报员101 们有没有查出结果来啊?@BE4 这么无耻下流道德败坏注册无数小号到处引战的人渣败类,不但诬陷了全体中国人,还诬陷小站是共青团的蜜罐,现在Google搜索‘开放区小站’头几条全是骂小站的黑屁文呀,这岂有此理!这不共戴天之仇,这还没查出@BE4 的大号岂可堰气息股、三八干修呢?
主要是不能移动平台,pc平台脚本有,但主力一直是用手机。。
Greasemonkey对android的支持要求firefox 57.0以上。
品葱官方可以考虑加入这个功能 @admin
可以保存在<script>直接加到<head>,但是不适合。
这个脚本保存过滤列表的方案有两个:
1,作为JSON保存在localStorage;
2,直接修改脚本内部的staticlist。
方案2是为了使脚本在浏览器拒绝网站数据时依然能够工作。
如果这个脚本被官方嵌入,那么用户当然无法更改脚本内容,方案2就无法工作,那么这个脚本就要求用户必须接受网站数据。
这是网站官方无法解决的问题,因为浏览器的隐私设置就是为此设计。
这个脚本保存过滤列表的方案有两个:
1,作为JSON保存在localStorage;
2,直接修改脚本内部的staticlist。
方案2是为了使脚本在浏览器拒绝网站数据时依然能够工作。
如果这个脚本被官方嵌入,那么用户当然无法更改脚本内容,方案2就无法工作,那么这个脚本就要求用户必须接受网站数据。
这是网站官方无法解决的问题,因为浏览器的隐私设置就是为此设计。
1:不管是什么方式的过滤,只要是需要访问者配置启用,都会遇到浏览器隐私设置的问题;
2:目前官方已经有折叠、封禁等功能来隐藏特定内容;
3:官方默认启用大规模非特定过滤违背了网站的建设初衷。
2:目前官方已经有折叠、封禁等功能来隐藏特定内容;
3:官方默认启用大规模非特定过滤违背了网站的建设初衷。
理论上是可行的,但实作效果不好。
能够在页面上直接获取的用户属性只有用户名和id,以这两个属性设置白名单显然不适合。
而其他属性则需要产生额外的网络访问,并且是每一个id发送一次,那么极端的情况是每页100次,对访问者的代价太大。
能够在页面上直接获取的用户属性只有用户名和id,以这两个属性设置白名单显然不适合。
而其他属性则需要产生额外的网络访问,并且是每一个id发送一次,那么极端的情况是每页100次,对访问者的代价太大。
关注列表的API是有的。
这个请求返回的是html的<li>,只有用户的用户名所以还要再请求一次来获取id,不过这个列表并不需要频繁更新所以问题不大。
不管是黑名单还是白名单,关键在于过滤规则是不是特定的。如果是非特定的,比如以用户的威望,那么在用户端这个规则就需要在每次执行时对数据进行更新。用户名也是一样,因为用户名是可更改的。
所以我在这个脚本中采用了对id进行判断的策略。
非特定过滤规则在服务器端的确很容易实现,但个人认为网站方不应这么做。
网站本身对于其托管的内容应当保持一定的中立。可以对于违反规则的用户或内容进行关闭,但除此之外的关闭行为就不合适了。
https://pincong.rocks/people/ajax/follows/?type=follows&uid={uid}
这个请求返回的是html的<li>,只有用户的用户名所以还要再请求一次来获取id,不过这个列表并不需要频繁更新所以问题不大。
不管是黑名单还是白名单,关键在于过滤规则是不是特定的。如果是非特定的,比如以用户的威望,那么在用户端这个规则就需要在每次执行时对数据进行更新。用户名也是一样,因为用户名是可更改的。
所以我在这个脚本中采用了对id进行判断的策略。
非特定过滤规则在服务器端的确很容易实现,但个人认为网站方不应这么做。
网站本身对于其托管的内容应当保持一定的中立。可以对于违反规则的用户或内容进行关闭,但除此之外的关闭行为就不合适了。