// ==UserScript==
// @name Hostloc根据关键字和用户名屏蔽帖子
// @namespace https://hostloc.com/
// @version 0.2.7
// @description 根据关键字和用户名屏蔽帖子,根据用户名屏蔽签名
// @author kiwi
// @homepage https://github.com/FlyxFly/hostloc-block-post-and-signature
// @match https://hostloc.com/forum-*
// @match https://hostloc.com/thread-*
// @match https://hostloc.com/forum.php?mod=viewthread&tid=*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @require https://unpkg.com/sweetalert/dist/sweetalert.min.js
// ==/UserScript==
(function() {
'use strict';
const now = function(){
return new Date().getTime();
}
// 移除数组中的空元素
if(!Array.prototype.trim){
Array.prototype.trim = function removeEmptyElements () {
return this.filter((x)=>{
return x;
})
}
}
// 查询数组是否存在某个值,忽略大小写
if(!Array.prototype.contains){
Array.prototype.contains = function checkIfInArray (target) {
for(let i=0;i<this.length;i++){
if(this[i].toUpperCase().includes(target.toUpperCase())){
return true;
}
}
return false
}
}
// 将html字符串转换为dom元素
const htmlToElement = function (html) {
const template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
class HostLocBlocker{
constructor(){
this.config={
blockedUser:[],
blockedKeyword:[],
blockedSignatureUser:[],
pantry:{
APIKey:null,
basket:null
}
}
this.dataKeys = ['blockedUser','blockedKeyword','blockedSignatureUser'];
this.contentStorage=[];
this.localStorageKey = 'hostlocBlockPlugin';
}
saveToLocal(){
const toBeStored = {
timestamp: now(),
data:this.config
}
localStorage.setItem(this.localStorageKey,JSON.stringify(toBeStored));
}
// 从localstorage读取数据并加载到this.config
restoreFromLocal(){
const data=localStorage.getItem(this.localStorageKey);
// 如果localStorage中没有数据,则初始化数据
if(!data){
this.saveToLocal();
return;
}
const jsonData=JSON.parse(data);
// 如果有指定键值不存在,则初始化数据
const keys = ['blockedUser', 'blockedKeyword', 'blockedSignatureUser'];
if(!keys.every(key => jsonData.data.hasOwnProperty(key))) {
this.saveToLocal();
return;
}
// 如果有 pantry 数据,则检查 pantry 数据是否过期,如果过期则从云端获取数据
if(jsonData.data.pantry.APIKey && jsonData.data.pantry.basket){
this.config.pantry = jsonData.data.pantry;
if((now() - jsonData.timestamp) > 12*3600*1000){
return this.modifyCloudData('get');
}
}
for (let x in this.dataKeys) {
const key = this.dataKeys[x];
this.config[key]=jsonData.data[key];
}
}
async modifyCloudData(action){
// 如果云端数据没有配置APIKey和basket,则保存到本地并提示用户
if(!this.config.pantry.APIKey || !this.config.pantry.basket){
this.saveToLocal();
swal({
text:'已保存到本地浏览器,清理浏览器缓存可能会导致数据丢失',
icon:'info',
button: '我知道了'
});
return false;
}
// 操作云端数据
const url = `https://getpantry.cloud/apiv1/pantry/${this.config.pantry.APIKey}/basket/${this.config.pantry.basket}`;
switch(action){
case 'get':
swal({
title:'正在获取云端数据',
text:'每天第一次打开需要同步数据,请稍后...',
icon:'info',
button:false
});
fetch(url,{
method:'GET',
headers: {
'Content-Type': 'application/json'
},
})
.then((response)=>{
return response.text()
})
.then((text)=>{
if(text.includes('pantry with id')){
throw new Error('Pantry Key 不存在,请检查');
}
if(text.includes('does not exist')){
throw new Error('Baskey 不存在,请检查');
}
const data = JSON.parse(text);
// 检查云端数据是否有对应键值,如果有就加载到本对象中
if(data.pantry && data.blockedKeyword && data.blockedSignatureUser && data.blockedUser){
this.config = data;
swal({
text:'云端数据加载成功',
icon:'success',
button:'好的'
});
}else{
swal({
text:'云端数据格式不正确,初始化数据',
icon:'info',
button: '我知道了'
})
}
// 如果云端数据结构不正确,则直接保存本对象自带的初始化数据
this.saveToLocal();
this.restoreFromLocal();
this.startBlockProcess();
console.log('Got data from cloud.',data);
}).catch((e)=>{
swal({
title:'云端数据加载失败',
text:e.message,
icon:'error',
button:'好的'
})
})
break;
case 'save':
swal({
text:'正在保存数据到云端,请稍后',
icon:'info',
button: false
});
fetch(url, {
method:'POST',
headers: {
'Content-Type': 'application/json'
},
body:JSON.stringify(this.config)
}).then((res)=>{
swal({
text:'数据保存到云端成功,可以跳转到其他页面了',
icon:'success',
button:'好的'
});
console.log('Save data to cloud',res);
}).catch((e)=>{
swal({
title:'保存失败',
text:e.message,
icon:'error',
button:'好的'
});
})
break;
default:
return false;
}
}
hideFromList(){
const blockedKeyword = this.config.blockedKeyword;
const blockedUser = this.config.blockedUser;
document.querySelectorAll('#threadlisttableid tbody').forEach((item,index)=>{
if(item.id.includes('normalthread')){
const title=item.querySelector('a.s.xst').innerText;
for (let i = blockedKeyword.length - 1; i >= 0; i--) {
if(title.toUpperCase().includes(blockedKeyword[i].toUpperCase())){
// item.querySelector('a.s.xst').innerText='已屏蔽';
item.style.display='none';
break;
}
}
const nameA=item.querySelectorAll('td.by')[0].querySelector('a');
if(nameA){
const userName=nameA.innerText.trim().toUpperCase();
if(blockedUser.contains(userName)){
// item.querySelector('a.s.xst').innerText='已屏蔽';
item.style.display='none';
}
}
}
})
}
hideReplyAndSignature(){
const blockedSignatureUser = this.config.blockedSignatureUser;
const blockedKeyword = this.config.blockedKeyword;
const contentStorage = this.contentStorage;
const blockedUser = this.config.blockedUser;
document.querySelectorAll('#postlist>div').forEach((post)=>{
if(!post.id.includes('post_')){
return false;
}
const userLink=post.querySelector('a.xw1');
if(userLink){
const userName=userLink.innerText.trim();
// 根据用户名屏蔽发帖
if(userName && blockedUser.includes(userName)){
post.style.display='none';
return false;
}
// 根据用户名屏蔽签名
if(blockedSignatureUser.includes(userName) && post.querySelector('div.sign')){
const signature=post.querySelector('div.sign');
const contentText=signature.innerText;
const contentHTML=signature.innerHTML;
const storageKey=post.id+'signature';
contentStorage[storageKey]=contentHTML;
signature.innerHTML=`<span style="font-style:italic;font-size:10px;color:gray" class="hidden-by-script" data-restore-key="${storageKey}" title="${contentText}">已屏蔽,鼠标移到此处查看内容,点击还原内容</span>`;
}
}
const tds=post.querySelectorAll('td');
tds.forEach((td)=>{
// 查找帖子内容容器: td.postmessage_{thread_id}
if(td.id.includes('postmessage_')){
const content=td.innerText;
for (let i = blockedKeyword.length - 1; i >= 0; i--) {
// 根据关键字屏蔽发帖内容
if(content.toUpperCase().includes(blockedKeyword[i].toUpperCase())){
const contentHTML=td.innerHTML;
const contentText=td.innerText;
contentStorage[post.id]=contentHTML;
td.innerHTML=`<span style="font-style:italic;font-size:10px;color:gray" class="hidden-by-script" data-restore-key="${post.id}" title="${content}">已屏蔽,鼠标移到此处查看内容,点击还原内容</span>`;
break;
}
}
}
})
})
}
addSettingButton(){
const p = document.querySelectorAll('#um p')[1];
p.appendChild(htmlToElement(`<span class="pipe">|</span>`));
p.appendChild(htmlToElement(`<a class="showmenu" id="show-block-panel">屏蔽名单设置</a>`));
}
addSettingPanel(){
const div = document.createElement('div');
div.id='hostloc-blocker-panel-wrapper';
div.innerHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="hostloc-blocker-panel" class="modal">
<div class="modal-title">
<p>Hostloc 屏蔽插件设置面板</p>
</div>
<div class="modal-content">
<div class="column api-setting">
<div class="field">
<label for="">Pantry API Key</label>
<input type="text" placeholder="没有可不填" name="pantry-api-key">
</div>
<div class="field">
<label for="">Pantry Basket</label>
<input type="text" placeholder="没有可不填" name="pantry-basket-name">
</div>
<p class="help">如填写Pantry API参数,可同时保存到云端。不填则仅保存到本地。<a href="https://getpantry.cloud/" target="_blank">点此注册</a></p>
<p class="help">如只填Pantry API参数,则从云端获取配置。</p>
</div>
<div class="columns">
<div class="column">
<label>屏蔽发帖</label>
<p class="help">每行一个<strong>区分大小写</strong></p>
<textarea name="blocked-user" id="blocked-user" cols="15" rows="13"></textarea>
</div>
<div class="column">
<label>屏蔽签名</label>
<p class="help">每行一个,<strong>区分大小写</strong></p>
<textarea name="blocked-signature-user" id="blocked-signature-user" cols="15" rows="13"></textarea>
</div>
<div class="column">
<label>屏蔽关键字</label>
<p class="help">每行一个,<strong>不分大小写</strong></p>
<textarea name="blocked-keyword" id="blocked-keyword" cols="15" rows="13"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button class="save">保存并关闭</button>
<p class="help">保存后刷新页面生效</p>
</div>
</div>
<style>
#hostloc-blocker-panel{
display: none;
background-color: white;
position: fixed;
left: 50%;
top: 100px;
transform: translateX(-50%);
padding: 20px;
width: 500px;
border-radius: 10px;
font-family:"Microsoft Yahei",Georgia, 'Times New Roman', Times, serif;
box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px;
}
#hostloc-blocker-panel.is-active{
display: block
}
#hostloc-blocker-panel .modal-content p{
margin-bottom: 0;
}
#hostloc-blocker-panel ::-webkit-scrollbar {
width: 10px;
}
#hostloc-blocker-panel .columns{
margin-top: 10px;
display: flex;
justify-content: space-evenly;
}
#hostloc-blocker-panel label{
font-weight: 700;
font-size: 15px;
}
#hostloc-blocker-panel .modal-title{
text-align: center;
border-bottom: 1px solid rgb(238, 238, 238);
}
#hostloc-blocker-panel .modal-content {
padding-bottom: 20px;
border-bottom: 1px solid rgb(238, 238, 238);
}
#hostloc-blocker-panel .modal-content .help{
color: rgb(43, 43, 43);
font-size: 11px;
margin-top: 0;
}
#hostloc-blocker-panel .modal-content .api-setting{
margin-top: 20px;
}
#hostloc-blocker-panel .modal-content .api-setting .field{
display: flex;
width: 100%;
position: relative;
height: 40px;
align-items: center;
}
#hostloc-blocker-panel .modal-content .api-setting label{
line-height: 40px;
display: block;
width: 30%;
height: 100%;
}
#hostloc-blocker-panel .modal-content .api-setting input{
height: 25px;
display: block;
width: 70%;
}
#hostloc-blocker-panel .modal-content .api-setting::after{
clear: both;
}
#hostloc-blocker-panel .modal-content textarea,
#hostloc-blocker-panel .modal-content input{
border:1px solid #ddd;
border-radius: 5px;
color: #363636;
border-color: 1px solid #b5b5b5;
font-size: 15px;
}
#hostloc-blocker-panel textarea:hover,
#hostloc-blocker-panel input:hover{
border-color: #b5b5b5;
}
#hostloc-blocker-panel textarea:focus,
#hostloc-blocker-panel input:focus{
outline:none
}
#hostloc-blocker-panel .modal-footer{
padding:30px 30px 0;
text-align: right;
}
#hostloc-blocker-panel .modal-footer button{
padding: 5px 15px;
border-color: #235994;
background-color: #06C;
background-position: 0 -48px;
color: #FFF;
}
</style>
</body>
</html>`;
document.body.appendChild(div);
}
addPanelEvents(){
const panel = document.querySelector('#hostloc-blocker-panel');
const saveButton = panel.querySelector('.save');
const inputPantryAPIKey = panel.querySelector('[name="pantry-api-key"]');
const inputBasketName = panel.querySelector('[name="pantry-basket-name"]');
const textareaBlockedUser = panel.querySelector('textarea[name="blocked-user"]');
const textareaBlockedSignatureUser = panel.querySelector('textarea[name="blocked-signature-user"]');
const textareaBlockedKeyword = panel.querySelector('textarea[name="blocked-keyword"]');
const openPanelLink = document.querySelector('#show-block-panel');
panel.addEventListener('click',(event)=>{
event.stopPropagation();
})
document.body.addEventListener('click',()=>{
panel.classList.remove('is-active');
})
openPanelLink.addEventListener('click',(event)=>{
event.stopPropagation();
textareaBlockedKeyword.value = this.config.blockedKeyword.join('\n');
textareaBlockedSignatureUser.value = this.config.blockedSignatureUser.join('\n');
textareaBlockedUser.value = this.config.blockedUser.join('\n');
inputPantryAPIKey.value = this.config.pantry.APIKey;
inputBasketName.value = this.config.pantry.basket;
panel.classList.add('is-active');
});
saveButton.addEventListener('click',()=>{
this.config.pantry.APIKey = inputPantryAPIKey.value;
this.config.pantry.basket = inputBasketName.value;
// 如果仅输入了apikey 则表示从云端拉取数据,将覆盖本地数据
if(!textareaBlockedKeyword.value && !textareaBlockedSignatureUser.value && !textareaBlockedUser.value){
this.modifyCloudData('get');
return;
}else{
// 将api key 和数据保存到本地
this.config.blockedKeyword = textareaBlockedKeyword.value.split('\n').trim();
this.config.blockedSignatureUser = textareaBlockedSignatureUser.value.split('\n').trim();
this.config.blockedUser = textareaBlockedUser.value.split('\n').trim();
// 保存到本地
this.saveToLocal();
// 启动屏蔽流程
this.startBlockProcess();
// 将数据保存到云端
this.modifyCloudData('save');
}
panel.classList.remove('is-active');
})
}
startBlockProcess(){
if(location.href.includes('forum')){
this.hideFromList();
}
if(location.href.includes('thread')){
this.hideReplyAndSignature();
}
//监听点击事件,恢复被屏蔽的签名和帖子
if(location.href.includes('thread')){
document.querySelector('#postlist').addEventListener('click',(e)=>{
const item=e.target;
if(item.className.includes('hidden-by-script')){
item.innerHTML=this.contentStorage[item.dataset.restoreKey];
item.title='';
item.style='';
}
})
}
}
init(){
this.addSettingPanel();
this.addSettingButton();
this.addPanelEvents();
this.restoreFromLocal();
this.startBlockProcess();
}
}
const app=new HostLocBlocker();
app.init();
})();