yyawf

Under construction

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name              yyawf
// @description       Under construction
// @namespace         https://github.com/tiansh
// @version           0.0.4
// @match             *://*.weibo.com/*
// @noframes
// @run-at            document-start
// @nocompat
// @connect           miaopai.com
// @connect           sina.cn
// @connect           sina.com.cn
// @connect           sinaimg.cn
// @connect           sinajs.cn
// @connect           t.cn
// @connect           weibo.com
// @author            田生 http://weibo.com/tsh90
// @license           MPL-2.0
// @grant             unsafeWindow
// @grant             GM_getValue
// @grant             GM_setValue
// ==/UserScript==

/*!
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/* eslint-env browser, greasemonkey */

const key = 'yawf-' + Array(64).fill(0).map(() => (Math.random() * 16).toString(16)[0]).join('');
const payload = ('void(' + function (config, key) {

  //#region utils
  const log = function (...args) {
    if (typeof args[0] === 'string') console.log('[yyawf] ' + args[0], ...args.slice(1));
    else console.log('[yyawf]', ...args);
  };
  const kebabCase = function (word) {
    if (typeof word !== 'string') return word;
    return word.replace(/./g, (char, index) => {
      const lower = char.toLowerCase();
      if (char === lower || index === 0) return lower;
      else return '-' + lower;
    });
  };
  //#endregion

  //#region vnode 处理
  /** @param {{ withSlot: boolean }} config */
  const allDescendants = (vnode, callback, config = {}) => {
    if (Array.isArray(vnode)) {
      vnode.forEach(child => allDescendants(child, callback, config));
    } else {
      callback(vnode);
      if (Array.isArray(vnode.children)) allDescendants(vnode.children, callback, config);
      else if (config?.withSlot && vnode.children && typeof vnode.children === 'object') {
        Object.keys(vnode.children).forEach(key => {
          slotContent(vnode, key, content => allDescendants(content, callback, config));
        });
      }
    }
    return vnode;
  };
  const slotContent = (vnode, key, callback) => {
    const slot = vnode.children?.[key];
    if (typeof slot !== 'function') return;
    vnode.children[key] = function (...args) {
      const result = slot.apply(this, args);
      return callback(result);
    };
  };
  //#endregion

  //#region 初始化
  /** @type {Promise<App>} */
  const appReady = new Promise(resolve => {
    const prevDesc = Object.getOwnPropertyDescriptor(Object.prototype, '$cookies');
    const isVueApp = obj => obj?._uid === 0;
    Object.defineProperty(Object.prototype, '$cookies', {
      set(value) {
        if (isVueApp(this)) {
          resolve(this);
          delete Object.prototype.$cookies;
          if (prevDesc && typeof prevDesc.set === 'function') {
            Object.defineProperty(Object.prototype, '$cookies', prevDesc);
          }
        } else if (prevDesc && typeof prevDesc.set === 'function') {
          prevDesc.set.call(this, value);
        } else {
          Object.defineProperty(this, '$cookies', { value, writable: true, configurable: true, enumerable: true });
        }
      },
      configurable: true,
      enumerable: false,
    });
  });
  //#endregion

  const keywords = typeof config.keywords === 'string' ? config.keywords.split(/[,,;;、]/).map(x => x.trim()).filter(x => x) : [];
  const feedFilter = function (feed) {
    if (feed.content_auth === 5) return { action: 'hide', reason: '广告' };
    if (feed.retweeted_status?.content_auth === 5) return { action: 'hide', reason: '广告' };
    const text = feed.text_raw || feed.text;
    const keywordMatch = keywords.find(keyword => text.includes(keyword));
    if (keywordMatch) return { action: 'hide', reason: `关键词“${keywordMatch}”` };
    const re_text = feed.retweeted_status?.text_raw || feed.retweeted_status?.text;
    const reKeywordMatch = re_text && keywords.find(keyword => re_text?.includes(keyword));
    if (reKeywordMatch) return { action: 'hide', reason: `关键词“${reKeywordMatch}”` };
    return { action: 'show' };
  };
  const commentFilter = function (comment) {
    const text = comment.text_raw || comment.text;
    const keywordMatch = keywords.find(keyword => text.includes(keyword));
    if (keywordMatch) return { action: 'hide', reason: `关键词“${keywordMatch}”` };
    return { action: 'show' };
  };
  const hotSearchFilter = function (hotSearch) {
    if (hotSearch.rank == null) return { action: 'hide', reason: '广告' };
    if (hotSearch.is_ad) return { action: 'hide', reason: '广告' };
    const text = hotSearch.word;
    const keywordMatch = keywords.find(keyword => text.includes(keyword));
    if (keywordMatch) return { action: 'hide', reason: `关键词“${keywordMatch}”` };
    return { action: 'show' };
  };

  //#region 监听组件生命周期
  /** @type {Record<LifecycleName, Record<string, Function[]>>} */
  const lifecycleListeners = {};
  const addLifecycleListener = (lifecycles, componentName, listener) => {
    const item = { listener };
    lifecycles.match(/\S+/g).forEach(lifecycle => {
      lifecycleListeners[lifecycle] ??= {};
      lifecycleListeners[lifecycle][componentName] ??= [];
      lifecycleListeners[lifecycle][componentName].push(item);
    });
    return () => {
      lifecycles.match(/\S+/g).forEach(lifecycle => {
        const index = lifecycleListeners[lifecycle][componentName].indexOf(item);
        if (index !== -1) lifecycleListeners[lifecycle][componentName].splice(index, 1);
      });
    };
  };
  const runLifecycleListeners = (lifecycle, instance) => {
    if (!instance?.render) return;
    const name = kebabCase(instance.type.name || instance.type.__name || instance.type.__refName);
    const listenersForAny = lifecycleListeners[lifecycle]?.['*'] ?? [];
    const listenersForComponent = lifecycleListeners[lifecycle]?.[name] ?? [];
    const listeners = [...listenersForAny, ...listenersForComponent];
    listeners.forEach(item => {
      try { item.listener(instance); } catch (E) { console.error(E); }
    });
  };
  const wrapRender = (instance, wrapper) => {
    if (!instance?.render) return;
    const render = wrapper(instance.render);
    render.raw = instance.render;
    instance.render = render;
    return render;
  };

  appReady.then(app => {
    app.mixin({
      beforeCreate() {
        runLifecycleListeners('beforeCreate', this?._);
      },
      created() {
        runLifecycleListeners('created', this?._);
      },
      mounted() {
        runLifecycleListeners('mounted', this?._);
      },
      beforeUpdate() {
        runLifecycleListeners('beforeUpdate', this?._);
      },
      updated() {
        runLifecycleListeners('updated', this?._);
      }
    });
  });
  //#endregion

  //#region DEBUG
  addLifecycleListener('mounted updated', '*', instance => {
    const el = instance.vnode.el;
    el.__vue__ = new WeakRef(instance);
  });
  //#endregion

  //#region 渲染增加组件名称
  const properName = result => {
    if (!result) return 'unnamed-component';
    const name = kebabCase(result.type?.name || result.type?.__name || result.type?.__refName);
    if (name) return name + '--child';
    return properName(result.parent);
  };
  const trimClassName = klass => klass.replace(/_[a-z\d]+_\d+$/, '');
  const tagRenderResult = prefix => vnode => {
    if (typeof vnode !== 'object' || !vnode) return vnode;
    if (Array.isArray(vnode)) return vnode.map(child => tagRenderResult(prefix)(child));
    // 更新 ClassName
    if (typeof vnode.props?.class === 'string') {
      const classList = vnode.props.class.split(' ').flatMap(klass => {
        if (!klass || klass.includes('__yawf_')) return [];
        if (!/_[a-z\d]+_\d+$/.test(klass)) return [klass];
        return [klass, '__yawf_' + prefix + '_' + trimClassName(klass)];
      }).filter(x => x);
      vnode.props.class = classList.join(' ');
    }
    // 标记事件处理
    if (vnode.props && typeof vnode.props === 'object') {
      Object.keys(vnode.props).forEach(eventName => {
        if (!eventName.startsWith('on') || eventName.startsWith('onUpdate')) return;
        const handler = vnode.props[eventName];
        const name = (typeof handler === 'function' ? [handler.name ?? 'function'] :
          Array.isArray(handler) ? handler.flatMap(item => typeof item === 'function' ? [(item.name ?? 'function')] : []) : []
        ).map(item => item.replace(/.*\s/, '')).join(' ');
        const key = kebabCase(eventName);
        vnode.props ??= {};
        vnode.props['__yawf_event_' + key.replace(/-/g, '_') + '__'] = name;
      });
    }
    // 递归内部元素
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        tagRenderResult(prefix)(child);
      });
    } else if (vnode.children && typeof vnode.children === 'object') {
      Object.keys(vnode.children).forEach(key => {
        const value = vnode.children[key];
        if (typeof value === 'function') {
          vnode.children[key] = function (...args) {
            return tagRenderResult(prefix)(value.apply(this, args));
          };
          vnode.children[key].raw = value;
        }
      });
    }
    return vnode;
  };
  addLifecycleListener('beforeCreate', '*', instance => {
    if (instance.type.components && typeof instance.type.components === 'object') {
      Object.keys(instance.type.components).forEach(key => {
        const value = instance.type.components[key];
        value.__refName = key;
      });
    }
    const name = kebabCase(instance.type.name || instance.type.__name || instance.type.__refName);
    const displayName = name || kebabCase(properName(instance));
    wrapRender(instance, function (render) {
      return function (...args) {
        const result = render.apply(this, args);
        tagRenderResult(displayName)(result);
        // 增加 Component Name
        if (typeof result.type === 'string') instance.__renderTag = result.type;
        else instance.__renderTag = null;
        result.props ??= {};
        for (let ins = instance, lv = 0; ins && (!lv || typeof ins.__renderTag !== 'string'); ins = ins.parent, ++lv) {
          const name = kebabCase(ins.type.name || ins.type.__name || ins.type.__refName || properName(ins));
          result.props['__yawf_component_' + name + '__'] = ins.uid;
          const key = ins.vnode.key;
          if (typeof key === 'string' || typeof key === 'number' || typeof key === 'symbol') {
            result.props['__yawf_key_' + (lv || '') + '__'] = String(key);
          }
          if (ins !== ins.parent?.children?.[0]) break;
        }
        if (result.key && (typeof result.key === 'string' || typeof result.key === 'number' || typeof result.key === 'symbol')) {
          result.props['__yawf_key__'] = String(result.key);
        }
        return result;
      };
    });
  });
  // 微博的操作按钮
  addLifecycleListener('mounted updated', 'feed-toolbar', instance => {
    const el = instance.vnode.el;
    [...el?.querySelectorAll?.('.__yawf_feed-toolbar__item')].forEach(item => {
      if (item.querySelector('[__yawf_component_woo-like__]')) item.setAttribute('__yawf_feed_toolbar__', 'like');
      else {
        const i = item.querySelector('i[class*="woo-font--"]');
        if (i) item.setAttribute('__yawf_feed_toolbar__', i.className.split(' ').find(i => i.includes('woo-font--')).split('--')[1]);
      }
    });
    const container = el.querySelector('.woo-box-flex').children;
    if (container[1]) container[1].setAttribute('__yawf_feed_toobar__extra__', '');
  });
  addLifecycleListener('mounted updated', 'icon-list', instance => {
    // 名字旁边的 Vip 等图标
    if (instance.type?.props?.icons) {
      const icons = instance.props.icons;
      const el = instance.vnode.el;
      if (Array.isArray(icons) && icons.length) {
        const children = el.children;
        icons.forEach((icon, index) => {
          children?.[index]?.setAttribute('__yawf_icon_list_item__', icon.type);
        })
      }
    }
    // 评论的操作按钮
    if (instance.type?.props?.iconsName) {
      const icons = instance.props.iconsName;
      const el = instance.vnode.el;
      if (Array.isArray(icons) && icons.length) {
        const children = el.children;
        icons.forEach((icon, index) => {
          children?.[index]?.setAttribute('__yawf_comment_toolbar_item__', icon.name);
        });
      }
    }
  });
  //#endregion

  //#region 设置入口
  addLifecycleListener('created', 'weibo-top-nav', instance => {
    instance.setupState?.configs.splice(-1, 0, {
      divider: true,
      href: '',
      name: '药方设置',
      type: 'yawf-config',
    });
    instance.ctx.configHandle = (function (_configHandle) {
      return function (...args) {
        const [index] = args;
        const type = instance.setupState.configs[index].type;
        if (type === 'yawf-config') {
          const event = new CustomEvent(key, { detail: { method: 'config' } });
          document.dispatchEvent(event);
        } else _configHandle.apply(this, args);
      };
    }(instance.ctx.configHandle)).bind(null);
  });
  //#endregion

  //#region 广告
  addLifecycleListener('created', 'card-hot-search', instance => {
    wrapRender(instance, function (render) {
      return function (...args) {
        const bandList = instance.data.bandList;
        const status = bandList.map(item => hotSearchFilter(item));
        if (status.some(item => item.action === 'hide')) {
          status.forEach((item, index) => { if (item.action === 'hide') log(`热搜过滤(${item.reason})`, bandList[index]); });
          bandList.splice(0, bandList.length, ...bandList.filter((item, index) => status[index].action !== 'hide'));
        }
        const result = render.apply(this, args);
        return result;
      };
    });
  });
  addLifecycleListener('beforeCreate', '*', instance => {
    const props = instance?.type?.props;
    if (props?.adHeight && props?.adBackground) {
      wrapRender(instance, function (render) {
        return function (...args) {
          if (instance.setupState.show) {
            instance.setupState.show = false;
            log('清理:广告:消息流头图');
          }
          const result = render.apply(this, args);
          return result;
        };
      });
    }
  });
  //#endregion

  //#region 消息流
  addLifecycleListener('beforeCreate', 'feed-scroll', instance => {
    wrapRender(instance, function (render) {
      return function (...args) {
        const feedList = instance.props.data;
        if (Array.isArray(feedList)) {
          const status = feedList.map(item => feedFilter(item));
          if (status.some(item => item.action === 'hide')) {
            status.forEach((item, index) => {
              if (item.action === 'hide') log(`微博过滤(${item.reason})`, feedList[index]);
            });
            const filtered = feedList.filter((item, index) => status[index].action !== 'hide');
            feedList.splice(0, feedList.length, ...filtered);
          }
        }
        const result = render.apply(this, args);
        return result;
      };
    });
  });
  //#endregion

  //#region 评论
  /*
  addLifecycleListener('beforeCreate', 'comment', instance => {
    wrapRender(instance, function (render) {
      return function (...args) {
        const comments = instance.data.commentData.comments;
        const status = comments.map(item => commentFilter(item));
        if (status.some(item => item.action === 'hide')) {
          status.forEach((item, index) => {
            if (item.action === 'hide') log(`评论过滤(${item.reason})`, comments[index]);
          });
          const filtered = comments.filter((item, index) => status[index].action !== 'hide');
          comments.splice(0, comments.length, ...filtered);
        }
        const result = render.apply(this, args);
        return result;
      };
    });
  });
  */
  //#endregion

  //#region 热搜固顶
  addLifecycleListener('created', 'card-hot-search', instance => {
    wrapRender(instance, function (render) {
      return function (...args) {
        if (instance.data.TopWords?.length) {
          log('清理:热搜:置顶热搜', instance.data.TopWords);
          instance.data.TopWords = [];
        }
        const result = render.apply(this, args);
        return result;
      };
    });
  });
  //#endregion

} + '(' + [(function () {
  try {
    const config = JSON.parse(GM_getValue('CONFIG_TEMP1'));
    if (typeof config === 'object') return config || {};
    return {};
  } catch { return {}; }
}()), key].map(x => JSON.stringify(x)) + '))');

const handlers = {};
document.addEventListener(key, event => {
  const { method } = event.detail;
  handlers[method]?.(event.detail.data);
});

handlers.config = () => {
  let config = {};
  try { config = JSON.parse(GM_getValue('CONFIG_TEMP1')); }
  catch { config = {}; }
  const keywords = prompt('现在 yyawf 尚处于原型验证阶段,所以还没有正经的设置窗口。你可以在下面逗号分隔的输入一些关键词,他们将会被用于过滤微博、评论和热搜话题。设置后刷新才会生效。这里的设置在后续版本中不会被保留。', config['keywords'] || '');
  if (keywords != null) config['keywords'] = keywords;
  GM_setValue('CONFIG_TEMP1', JSON.stringify(config));
};

unsafeWindow.eval(payload);