yyawf

Under construction

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name              yyawf
// @description       Under construction
// @namespace         https://github.com/tiansh
// @version           0.0.2
// @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) {

  const log = function (...args) {
    if (typeof args[0] === 'string') console.log('[yyawf] ' + args[0], ...args.slice(1));
    else console.log('[yyawf]', ...args);
  };

  //#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 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;
    });
  };

  const keywords = typeof config.keywords === 'string' ? config.keywords.split(/[,,;;、]/).map(x => x.trim()).filter(x => x) : [];
  log('keywords', keywords);

  const feedFilter = function (feed) {
    if (feed.content_auth === 5) return 'hide';
    if (feed.retweeted_status?.content_auth === 5) return 'hide';
    const text = feed.text_raw || feed.text;
    if (keywords.some(keyword => text.includes(keyword))) return 'hide';
    const re_text = feed.retweeted_status?.text_raw || feed.retweeted_status?.text;
    if (re_text && keywords.some(keyword => re_text.includes(keyword))) return 'hide';
    return 'show';
  };

  const hotSearchFilter = function (hotSearch) {
    if (hotSearch.rank == null) return 'hide';
    if (hotSearch.is_ad) return 'hide';
    const text = hotSearch.word;
    if (keywords.some(keyword => text.includes(keyword))) return 'hide';
    return 'show';
  };

  //#region 监听组件生命周期
  /** @type {Record<LifecycleName, Record<string, Function[]>>} */
  const lifecycleListeners = {};
  const addLifecycleListener = (lifecycle, componentName, listener) => {
    const item = { listener };
    lifecycleListeners[lifecycle] ??= {};
    lifecycleListeners[lifecycle][componentName] ??= [];
    lifecycleListeners[lifecycle][componentName].push(item);
    return () => {
      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);
    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?._);
      },
    });
  });
  //#endregion

  //#region 渲染增加组件名称
  const trimClassName = klass => klass.replace(/_[a-z\d]+_\d+$/, '');
  const normalizeClassName = prefix => vnode => {
    if (typeof vnode !== 'object' || !vnode) return vnode;
    if (typeof vnode.props?.class === 'string') {
      const classList = vnode.props.class.split(' ').map(klass => {
        if (klass.includes('__yawf_')) return '';
        return klass + ' ' + '__yawf_' + prefix + '_' + trimClassName(klass);
      }).filter(x => x);
      vnode.props.class = classList.join(' ');
    }
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        normalizeClassName(prefix)(child);
      });
    }
    return vnode;
  };
  addLifecycleListener('beforeCreate', '*', instance => {
    const name = kebabCase(instance.type.name || instance.type.__name);
    if (name) wrapRender(instance, function (render) {
      return function (...args) {
        const result = render.apply(this, args);
        normalizeClassName(name)(result);
        return result;
      };
    });
  });
  //#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) {
        log('card-hot-search bandList', instance.data.bandList);
        const bandList = instance.data.bandList;
        const status = bandList.map(item => hotSearchFilter(item));
        if (status.some(item => item === 'hide')) {
          log('Hot Search List Cleaned');
          bandList.splice(0, bandList.length, ...bandList.filter((item, index) => status[index] !== '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('Ad Image Removed');
          }
          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 === 'hide')) {
            log('Feed List Cleaned');
            status.forEach((item, index) => {
              if (item === 'hide') log('Feed Removed', feedList[index]);
            });
            const filtered = feedList.filter((item, index) => status[index] !== 'hide');
            feedList.splice(0, feedList.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) {
        log('card-hot-search TopWords', instance.data.TopWords);
        if (instance.data.TopWords?.length) {
          log('Top Hot Search Removed');
          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']);
  config['keywords'] = keywords;
  GM_setValue('CONFIG_TEMP1', JSON.stringify(config));
};

unsafeWindow.eval(payload);