Greasy Fork

Greasy Fork is available in English.

去广告&关键词屏蔽

去除“全部关注”和“最新微博”列表中的广告&屏蔽包含设置的关键词的微博/用户

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              去广告&关键词屏蔽
// @namespace         Violentmonkey Scripts
// @version           4.5
// @description       去除“全部关注”和“最新微博”列表中的广告&屏蔽包含设置的关键词的微博/用户
// @description:zh    去除“全部关注”和“最新微博”列表中的广告&屏蔽包含设置的关键词的微博/用户
// @author            fbz
// @match             *://*.weibo.com/*
// @exclude           *://weibo.com/tv*
// @grant             none
// @noframes
// @require           https://unpkg.com/[email protected]/dist/ajaxhook.js
// @require           https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js
// ==/UserScript==
/* jshint esversion: 6 */
;(function () {
  /*添加样式*/
  var css = `
    #add_ngList_btn {
      position: fixed;
      bottom: 2rem;
      left: 1rem;
      width: 2rem;
      height: 2rem;
      border-radius: 50%;
      border: 1px solid rgba(0, 0, 0, 0.5);
      background: white;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      z-index: 100;
    }

    #add_ngList_btn::before {
      content: '';
      position: absolute;
      width: 16px;
      height: 2px;
      background: rgba(0, 0, 0, 0.5);
      top: calc(50% - 1px);
      left: calc(50% - 8px);
    }

    #add_ngList_btn::after {
      content: '';
      position: absolute;
      height: 16px;
      width: 2px;
      background: rgba(0, 0, 0, 0.5);
      top: calc(50% - 8px);
      left: calc(50% - 1px);
    }

    .my-dialog__wrapper {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      overflow: auto;
      margin: 0;
      z-index: 10000;
      background: rgba(0, 0, 0, 0.3);
      display: none;
    }

    .my-dialog {
      position: relative;
      background: #FFFFFF;
      border-radius: 2px;
      box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
      box-sizing: border-box;
      width: 50%;
      transform: none;
      left: 0;
      margin: 0 auto;
    }

    .my-dialog .my-dialog__header {
      border-bottom: 1px solid #e4e4e4;
      padding: 14px 16px 10px 16px;
    }

    .my-dialog__title {
      line-height: 24px;
      font-size: 18px;
      color: #303133;
    }

    .my-dialog__headerbtn {
      position: absolute;
      top: 20px;
      right: 20px;
      padding: 0;
      background: transparent;
      border: none;
      outline: none;
      cursor: pointer;
      font-size: 16px;
      width: 12px;
      height: 12px;
      transform: rotateZ(45deg);
    }

    .my-dialog .my-dialog__header .my-dialog__headerbtn {
      right: 16px;
      top: 16px;
    }

    .my-dialog__headerbtn .my-dialog__close::before {
      content: '';
      position: absolute;
      width: 12px;
      height: 1.5px;
      background: #909399;
      top: calc(50% - 0.75px);
      left: calc(50% - 6px);
      border-radius: 2px;
    }

    .my-dialog__headerbtn:hover .my-dialog__close::before {
      background: #1890ff;
    }

    .my-dialog__headerbtn .my-dialog__close::after {
      content: '';
      position: absolute;
      height: 12px;
      width: 1.5px;
      background: #909399;
      top: calc(50% - 6px);
      left: calc(50% - 0.75px);
      border-radius: 2px;
    }

    .my-dialog__headerbtn:hover .my-dialog__close::after {
      background: #1890ff;
    }

    .my-dialog__body {
      padding: 30px 20px;
      color: #606266;
      font-size: 14px;
      word-break: break-all;
    }

    .my-dialog__footer {
      padding: 20px;
      padding-top: 10px;
      text-align: right;
      box-sizing: border-box;
    }

    .my-dialog .my-dialog__footer {
      padding: 0px 16px 24px 16px;
      margin-top: 40px;
    }

    #ngList {
      display: flex;
      flex-wrap: wrap;
      justify-content: flex-start;
      max-height: 480px;
      overflow-y: scroll;
    }

    .close-icon {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      display: inline-block;
      position: relative;
      transform: rotateZ(45deg);
      margin-left: 8px;
      cursor: pointer;
    }

    .close-icon:hover {
      background: #409eff;
    }

    .close-icon::before {
      content: '';
      position: absolute;
      width: 8px;
      height: 2px;
      background: #409eff;
      top: calc(50% - 1px);
      left: calc(50% - 4px);
      border-radius: 2px;
    }

    .close-icon:hover::before {
      background: #fff;
    }

    .close-icon::after {
      content: '';
      position: absolute;
      height: 8px;
      width: 2px;
      background: #409eff;
      top: calc(50% - 4px);
      left: calc(50% - 1px);
      border-radius: 2px;
    }

    .close-icon:hover::after {
      background: #fff;
    }

    .ng_item {
      background-color: #ecf5ff;
      display: inline-flex;
      align-items: center;
      padding: 0 10px;
      font-size: 12px;
      color: #409eff;
      border: 1px solid #d9ecff;
      border-radius: 4px;
      box-sizing: border-box;
      white-space: nowrap;
      height: 28px;
      line-height: 26px;
      margin-left: 12px;
      margin-top: 8px;
    }


    .input_container {
      display: flex;
      align-items: center;
      margin-bottom: 12px;
    }

    .el-input {
      position: relative;
      font-size: 14px;
      display: inline-block;
      width: 100%;
    }

    .el-input__inner {
      -webkit-appearance: none;
      background-color: #fff;
      background-image: none;
      border-radius: 4px;
      border: 1px solid #dcdfe6;
      box-sizing: border-box;
      color: #606266;
      display: inline-block;
      font-size: inherit;
      height: 40px;
      line-height: 40px;
      outline: none;
      padding: 0 15px;
      transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
      width: 100%;
      cursor: pointer;
      font-family: inherit;
    }

    .el-button {
      display: inline-block;
      line-height: 1;
      white-space: nowrap;
      cursor: pointer;
      background: #fff;
      border: 1px solid #dcdfe6;
      color: #606266;
      -webkit-appearance: none;
      text-align: center;
      box-sizing: border-box;
      outline: none;
      margin: 0;
      transition: .1s;
      font-weight: 500;
      -moz-user-select: none;
      -webkit-user-select: none;
      -ms-user-select: none;
      padding: 12px 20px;
      font-size: 14px;
      border-radius: 4px;
    }

    .el-button:focus,
    .el-button:hover {
      color: #409eff;
      border-color: #c6e2ff;
      background-color: #ecf5ff;
    }

    .el-button:active {
      color: #3a8ee6;
      border-color: #3a8ee6;
      outline: none;
    }

    .input_container .el-input {
      margin-right: 12px;
    }

    .tips {
      margin-top: 24px;
      font-size: 12px;
      color: #F56C6C;
    }
  `
  /*添加样式*/
  function addStyle(css) {
    if (!css) return
    var head = document.querySelector('head')
    var style = document.createElement('style')
    style.type = 'text/css'
    style.innerHTML = css
    head.appendChild(style)
  }

  /*dialog模板*/
  var dialog_temp = `
    <div class="my-dialog" style="margin-top: 15vh; width: 40%;">
      <div class="my-dialog__header">
        <span class="my-dialog__title">屏蔽词列表</span>
        <button type="button" aria-label="Close" class="my-dialog__headerbtn">
          <i class="my-dialog__close"></i>
        </button>
      </div>
      <div class="my-dialog__body">
        <div class="input_container">
          <div class="el-input">
            <input id="ngWord_input" class="el-input__inner" type="text" />
          </div>
          <button type="button" class="el-button" id="add_btn">
            <span>添加</span>
          </button>
        </div>
        <div id="ngList"></div>
        <p class="tips">注:1. 可过滤包含屏蔽词的用户、微博、评论、热搜。 2. 关键词保存在本地的local storage中。 3. 更改关键词后刷新页面生效(不刷新页面的情况下,只有之后加载的微博才会生效)。</p>
      </div>
      <div class="my-dialog__footer"></div>
    </div>
  `
  
  /*生成添加屏蔽关键词的按钮*/
  function createngListBtn() {
    var btn = document.createElement('div')
    btn.title = '添加屏蔽关键词'
    var span = document.createElement('span')
    span.innerText = ''
    btn.appendChild(span)
    btn.id = 'add_ngList_btn'
    document.body.appendChild(btn)

    /*初始化事件*/
    // 点击按钮展示弹窗
    btn.addEventListener('click', function () {
      showDialog()
    })
  }

  var NgListKey = 'NgList'

  var apiBlackList = ['/female_version.mp3', '/intake/v2/rum/events'] // 接口黑名单

  // 获取屏蔽词列表
  function getNgList() {
    // return JSON.parse(localStorage.getItem(NgListKey))
    var res = Cookies.get(NgListKey, { domain: '.weibo.com' })
    return res ? JSON.parse(res) : res
  }

  // 设置屏蔽词值
  function setNgList(list) {
    localStorage.setItem(NgListKey, JSON.stringify(list))
    return Cookies.set(NgListKey, JSON.stringify(list), {
      domain: '.weibo.com',
      sameSite: 'none',
      secure: true,
    })
  }

  // 初始化屏蔽词
  function initNgList() {
    // 从localhost同步到cookies
    var initList = JSON.parse(localStorage.getItem(NgListKey))
    if (initList && initList.length > 0) {
      setNgList(initList)
    } else {
      setNgList([])
    }
  }

  // 初始化dialog
  function initDialog() {
    var wrapper = document.createElement('div')
    wrapper.classList.add('my-dialog__wrapper')
    wrapper.innerHTML = dialog_temp
    document.body.appendChild(wrapper)

    /*初始化事件*/
    document
      .querySelector('.my-dialog__headerbtn')
      .addEventListener('click', function () {
        // 关闭按钮点击事件
        hideDialog()
      })
    document.querySelector('#add_btn').addEventListener('click', function () {
      // 添加关键词按钮点击事件
      var ngWord_input = document.querySelector('#ngWord_input')

      if (ngWord_input && ngWord_input.value) {
        data.ngList = ngList.concat([ngWord_input.value.trim()])
        ngWord_input.value = ''
      }
    })
  }
  // 显示dialog
  function showDialog() {
    data.ngList = data.ngList
    document.querySelector('.my-dialog__wrapper').style.display = 'initial'
  }
  // 隐藏dialog
  function hideDialog() {
    document.querySelector('.my-dialog__wrapper').style.display = ''
  }

  // 把屏蔽词列表添加到弹窗中
  function setNgListToDom(list) {
    var nodeStr = ''
    for (var [i, item] of list.entries()) {
      nodeStr += `<span class="ng_item">${item}<i class="close-icon" data-index=${i}></i></span>`
    }
    var ngListNode = document.querySelector('#ngList')
    if (ngListNode) {
      ngListNode.innerHTML = nodeStr

      var onDel = (i) => {
        // 删除关键词
        var arr = [...data.ngList]
        arr.splice(i, 1)
        data.ngList = arr || []
      }
      var delBtnList = ngListNode.querySelectorAll('.close-icon')
      for (var [i, node] of delBtnList.entries()) {
        node.addEventListener('click', function (el) {
          onDel(Number(el.target.dataset.index))
        })
      }
    }
  }

  var data = {
    ngList: []
  }

  Object.defineProperty(data, 'ngList', {
    // 简易双向绑定
    get: function () {
      return ngList
    },
    set: function (value) {
      ngList = value || []
      setNgList(ngList)
      setNgListToDom(ngList)
    },
  })

  window.addEventListener('load', function () {
    addStyle(css) // 添加样式
    createngListBtn() // 生成按钮
    initDialog() // 初始化弹窗
    // 首页、热搜页面观察器
    appObserverInit() // 屏蔽视频播放后的弱智三连语音、过滤热搜
    // 搜索页观察器
    searchObserverInit()
    // 热搜页观察器
    hotObserverInit()
  })

  // 创建首页观察器
  function appObserverInit() {
    var targetNode = document.getElementById('app')
    // 观察器的配置(需要观察什么变动)
    var config = {
      childList: true,
      subtree: true,
    }
    // 当观察到变动时执行的回调函数
    var callback = function (mutationsList, observer) {
      var audioList = document.querySelectorAll('.AfterPatch_bg_34rqc')
      for (var audio of audioList) {
        audio.remove()
        console.log('移除了弱智三连')
      }
    }

    // 创建一个观察器实例并传入回调函数
    var observer = new MutationObserver(callback)

    // 以上述配置开始观察目标节点
    targetNode && observer.observe(targetNode, config)
  }
  // 搜索页观察器
  function searchObserverInit() {
    var targetNode = document.getElementById('pl_feedlist_index')
    // 观察器的配置(需要观察什么变动)
    var config = {
      childList: true,
      subtree: true,
    }
    // 当观察到变动时执行的回调函数
    var callback = function (mutationsList, observer) {
      var searchList = targetNode.querySelectorAll('.card-wrap')
      for (var search of searchList) {
        var text = search.innerText
        if (ngList.some((word) => text.includes(word))) {
          search.style.display = 'none'
        }
      }
    }

    // 创建一个观察器实例并传入回调函数
    var observer = new MutationObserver(callback)
    // 以上述配置开始观察目标节点
    if (targetNode) {
      observer.observe(targetNode, config)
      // 手动让搜索页变化,触发观察器
      var span = document.createElement('span')
      targetNode.appendChild(span)
    }
  }
  // 热搜页观察器
  function hotObserverInit() {
    var targetNode = document.getElementsByClassName('Main_full_1dfQX')[0]
    // 观察器的配置(需要观察什么变动)
    var config = {
      childList: true,
      subtree: true,
    }
    // 当观察到变动时执行的回调函数
    var callback = function (mutationsList, observer) {
      var hotList = targetNode.querySelectorAll('.vue-recycle-scroller__item-view')
      for (var hot of hotList) {
        var text = hot.innerText
        if (ngList.some((word) => text.includes(word))) {
          hot.style.opacity = 0
        }
      }
    }

    // 创建一个观察器实例并传入回调函数
    var observer = new MutationObserver(callback)
    // 以上述配置开始观察目标节点
    targetNode && observer.observe(targetNode, config)
  }

  var ngList = getNgList() // 屏蔽词列表

  if (!ngList) {
    initNgList()
    ngList = getNgList()
  }
  data.ngList = ngList

  function responseFilter(response) {
    // 请求过滤方法
    var url =
      typeof response.responseURL === 'string' ? response.responseURL : ''
    var res = response.response

    if (res) {
      res = JSON.parse(res)
      var ngList = getNgList()
      var containsNgWord = (text) =>
        ngList.some((word) => text?.includes(word))

      var filterStatuses = (statuses, isHot) => {
        return statuses.reduce((acc, cur) => {
          // 热搜允许展示未关注人
          if (isHot || cur.user.following || cur.screen_name_suffix_new) {
            var myText = cur.text || ''
            var ngWordInMyText =
              containsNgWord(myText) ||
              (cur.user?.screen_name && containsNgWord(cur.user.screen_name))

            if (!ngWordInMyText) {
              if (cur.retweeted_status) {
                var oriText = cur.retweeted_status.text || ''
                var ngWordInOriText =
                  containsNgWord(oriText) ||
                  (cur.retweeted_status?.user?.screen_name &&
                    containsNgWord(cur.retweeted_status.user.screen_name))

                if (ngWordInOriText) return acc
              }
              acc.push(cur)
            }
          }
          return acc
        }, [])
      }

      var filterComments = (comments) => {
        return comments.reduce((acc, cur) => {
          if (
            !containsNgWord(cur.text) &&
            !(cur.user?.screen_name && containsNgWord(cur.user.screen_name))
          ) {
            if (cur.comments) {
              cur.comments = filterComments(cur.comments)
            } else if (cur.reply_comment?.comment_badge) {
              cur.reply_comment.comment_badge = filterComments(
                cur.reply_comment.comment_badge
              )
            }
            acc.push(cur)
          }
          return acc
        }, [])
      }

      var filterSearchBand = (searchBands) => {
        return searchBands.reduce((acc, cur) => {
          if (!containsNgWord(cur.word)) {
            acc.push(cur)
          }
          return acc
        }, [])
      }
      var filterNews = (news) => {
        return news.reduce((acc, cur) => {
          if (!containsNgWord(cur.topic)) {
            acc.push(cur)
          }
          return acc
        }, [])
      }
      if (url.includes('/friendstimeline') || url.includes('/unreadfriendstimeline') || url.includes('/hottimeline') || url.includes('/groupstimeline')) {
        if (url.includes('m.weibo.cn')) {
          res.data.statuses = filterStatuses(res.data.statuses)
        } else {
          res.statuses = filterStatuses(
            res.statuses,
            url.includes('/hottimeline')
          )
        }
      } else if (url.includes('/buildComments')) {
        res.data = filterComments(res.data)
      } else if (url.includes('/searchBand') || url.includes('/mineBand') || url.includes('/hotSearch')) {
        res.data.realtime = filterSearchBand(res.data.realtime)
      } else if (url.includes('/entertainment')) {
        res.data.band_list = filterSearchBand(res.data.band_list)
      } else if (url.includes('/news')) {
        res.data.band_list = filterNews(res.data.band_list)

      }

      return JSON.stringify(res)
    }
  }
  function initHook() {
    ah.hook({
      onloadend: function (xhr) {
        //拦截回调
        this.responseText = responseFilter(xhr)
      },
      open: function (arg, xhr) {
        var url = arg[1] || ''
        //返回true则表示阻断
        return apiBlackList.some((item) => url.toString().includes(item))
      }
    })
  }
  initHook()
})()