去广告&关键词屏蔽

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

// ==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/ajax-hook@3.0.3/dist/ajaxhook.js
// @require           https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/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()
})()