NGA Filter

troll must die

As of 2020-02-03. See the latest version.

// ==UserScript==
// @name        NGA Filter
// @namespace   https://greasyfork.org/users/263018
// @version     1.0.4
// @author      snyssss
// @description troll must die

// @match       *bbs.nga.cn/thread.php?fid=*
// @match       *bbs.nga.cn/read.php?tid=*
// @match       *bbs.nga.cn/nuke.php?*
// @match       *ngabbs.com/thread.php?fid=*
// @match       *ngabbs.com/read.php?tid=*
// @match       *ngabbs.com/nuke.php?*

// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue

// @noframes
// ==/UserScript==

((n, self) => {
  "use strict";

  if (n === undefined) return;

  const key = "NGAFilter";

  // 数据
  const data = (() => {
    const d = {
      tags: {},
      users: {},
      options: {
        filterMode: 0,
        keyword: ""
      }
    };
    const v = GM_getValue(key);
    if (typeof v !== "object") {
      return d;
    }
    return Object.assign(d, v);
  })();

  // 保存数据
  const saveData = () => {
    GM_setValue(key, data);
  };

  // 增加标记
  const addTag = name => {
    const tag = Object.values(data.tags).find(item => item.name === name);

    if (tag) return tag.id;

    const id =
      Math.max(...Object.values(data.tags).map(item => item.id), 0) + 1;

    const hash = (() => {
      let h = 5381;
      for (var i = 0; i < name.length; i++) {
        h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
      }
      return h;
    })();

    const hex = Math.abs(hash).toString(16) + "000000";

    const hsv = [
      `0x${hex.substr(2, 2)}` / 255,
      `0x${hex.substr(2, 2)}` / 255 / 2 + 0.25,
      `0x${hex.substr(4, 2)}` / 255 / 2 + 0.25
    ];

    const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]);

    const color = ["#", ...rgb].reduce((a, b) => {
      return a + ("0" + b.toString(16)).slice(-2);
    });

    data.tags[id] = {
      id,
      name,
      color,
      enabled: true
    };

    saveData();

    return id;
  };

  // 增加用户
  const addUser = (id, name = null, tags = [], isEnabled = true) => {
    if (data.users[id]) return data.users[id];

    data.users[id] = {
      id,
      name,
      tags,
      enabled: isEnabled
    };

    saveData();

    return data.users[id];
  };

  // 旧版本数据迁移
  {
    const dataKey = "troll_data";
    const modeKey = "troll_mode";
    const keywordKey = "troll_keyword";

    if (localStorage.getItem(dataKey)) {
      let trollMap = (function() {
        try {
          return JSON.parse(localStorage.getItem(dataKey)) || {};
        } catch (e) {}

        return {};
      })();

      let filterMode = ~~localStorage.getItem(modeKey);

      let filterKeyword = localStorage.getItem(keywordKey) || "";

      // 整理标签
      [...new Set(Object.values(trollMap).flat())].forEach(item =>
        addTag(item)
      );

      // 整理用户
      Object.keys(trollMap).forEach(item => {
        addUser(
          item,
          null,
          trollMap[item].map(tag => addTag(tag))
        );
      });

      data.options.filterMode = filterMode ? 0 : 1;
      data.options.keyword = filterKeyword;

      localStorage.removeItem(dataKey);
      localStorage.removeItem(modeKey);
      localStorage.removeItem(keywordKey);

      saveData();
    }
  }

  // 编辑用户标记
  const editUser = (() => {
    let window;
    return (uid, name, callback) => {
      if (window === undefined) {
        window = n.createCommmonWindow();
      }

      const user = data.users[uid];

      const content = document.createElement("div");

      content.className = "w100";
      content.innerHTML = `
        <table class="filter-table" style="min-width: 400px;">
            <tbody>
                ${Object.values(data.tags)
                  .map(
                    tag => `
                        <tr>
                            <td>
                                <b class="block_txt nobr" style="background:${
                                  tag.color
                                }; color:#fff; margin: 0.1em 0.2em;">${
                      tag.name
                    }</b>
                            </td>
                            <td>
                                <input type="checkbox" value="${
                                  tag.id
                                }" ${user &&
                      user.tags.find(item => item === tag.id) &&
                      "checked"}/>
                            </td>
                        </tr>
                    `
                  )
                  .join("")}
            </tbody>
            <tfoot>
                <tr>
                <td colspan="2">
                    <input placeholder="一次性添加多个标记用&quot;|&quot;隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
                </td>
                </tr>
            </tfoot>
        </table>
        <div style="margin: 10px 0;">
            <button>${user && user.enabled === false ? "启用" : "禁用"}</button>
            <div class="right_">
                <button>删除</button>
                <button>保存</button>
            </div>
        </div>
    `;

      const actions = content.getElementsByTagName("button");

      actions[0].onclick = () => {
        actions[0].innerText =
          actions[0].innerText === "禁用" ? "启用" : "禁用";
      };

      actions[1].onclick = () => {
        if (confirm("是否确认?")) {
          delete data.users[uid];

          saveData();

          callback && callback();

          window._.hide();
        }
      };

      actions[2].onclick = () => {
        if (confirm("是否确认?")) {
          const values = [...content.getElementsByTagName("input")];
          const newTags = values[values.length - 1].value
            .split("|")
            .filter(item => item.length)
            .map(item => addTag(item));
          const tags = [
            ...new Set(
              values
                .filter(item => item.type === "checkbox" && item.checked)
                .map(item => ~~item.value)
                .concat(newTags)
            )
          ].sort();

          if (user) {
            user.tags = tags;
            user.enabled = actions[0].innerText === "禁用";
          } else {
            addUser(uid, name, tags, actions[0].innerText === "禁用");
          }

          saveData();

          callback && callback();

          window._.hide();
        }
      };

      if (user === undefined) {
        actions[1].style = "display: none;";
      }

      window._.addContent(null);
      window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`);
      window._.addContent(content);
      window._.show();
    };
  })();

  // 过滤
  const reFilter = (() => {
    const tPage = location.pathname === "/thread.php";
    const pPage = location.pathname === "/read.php";
    const uPage = location.pathname === "/nuke.php";

    const func = (() => {
      if (tPage) {
        return () => {
          const tData = n.topicArg.data;

          Object.values(tData).forEach(item => {
            const uid =
              item[2].search.match(/uid=(\S+)/) &&
              item[2].search.match(/uid=(\S+)/)[1];

            const user = data.users[uid];

            const tags = user ? user.tags.map(tag => data.tags[tag]) : [];

            const isBlock =
              (user &&
                user.enabled &&
                (tags.length === 0 ||
                  tags.filter(tag => tag.enabled).length)) ||
              (data.options.keyword.length &&
                item[1].innerText.search(data.options.keyword) >= 0);

            item.contentC = item[1];

            item.contentB = item.contentB || item.contentC.innerHTML;

            item.containerC =
              item.containerC || item.contentC.parentNode.parentNode;

            item.containerC.style =
              isBlock && data.options.filterMode === 0 ? "display: none;" : "";

            item.contentC.style =
              isBlock && data.options.filterMode === 1
                ? "text-decoration: line-through;"
                : "";
          });
        };
      } else if (pPage) {
        return () => {
          const pData = n.postArg.data;

          Object.values(pData).forEach(item => {
            if (~~item.pAid === self) return;

            if (typeof item.i === "number") {
              item.actionC =
                item.actionC ||
                (() => {
                  const ele = item.uInfoC.firstElementChild.lastElementChild;

                  ele.onclick = null;

                  return ele;
                })();

              item.tagC =
                item.tagC ||
                (() => {
                  const tc = document.createElement("div");

                  tc.className = "filter-tags";

                  item.uInfoC.appendChild(tc);

                  return tc;
                })();
            }

            item.pName =
              item.pName ||
              item.uInfoC.getElementsByClassName("author")[0].innerText;

            item.reFilter =
              item.reFilter ||
              (() => {
                const user = data.users[item.pAid];

                const tags = user ? user.tags.map(tag => data.tags[tag]) : [];

                const isBlock =
                  user &&
                  user.enabled &&
                  (tags.length === 0 || tags.filter(tag => tag.enabled).length);

                item.avatarC =
                  item.avatarC ||
                  (() => {
                    const tc = document.createElement("div");

                    const avatar = document.getElementById(
                      `posteravatar${item.i}`
                    );

                    if (avatar) {
                      avatar.parentNode.insertBefore(tc, avatar.nextSibling);

                      tc.appendChild(avatar);
                    }

                    return tc;
                  })();

                item.contentB = item.contentB || item.contentC.innerHTML;

                item.containerC =
                  item.containerC ||
                  (() => {
                    let temp = item.contentC;

                    while (
                      temp.className !== "forumbox postbox" &&
                      temp.className !== "comment_c left"
                    ) {
                      temp = temp.parentNode;
                    }

                    return temp;
                  })();

                item.containerC.style.display =
                  isBlock && data.options.filterMode === 0 ? "none" : "";

                item.contentC.innerHTML =
                  isBlock && data.options.filterMode === 1
                    ? `
                    <div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
                        <span class="crimson">Troll must die.</span>
                        <a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${user.id}')].forEach(item => item.style.display = '')">点击查看</a>
                        <div style="display: none;" name="troll_${user.id}">
                            ${item.contentB}
                        </div>
                    </div>`
                    : item.contentB;

                item.avatarC.style.display = isBlock ? "none" : "";

                if (item.actionC) {
                  item.actionC.style =
                    user && user.enabled
                      ? "background: #cb4042;"
                      : "background: #aaa;";
                }

                if (item.tagC) {
                  item.tagC.style.display = tags.length ? "" : "none";
                  item.tagC.innerHTML = tags
                    .map(
                      tag =>
                        `<b class="block_txt nobr" style="background:${tag.color}; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>`
                    )
                    .join("");
                }
              });

            if (item.actionC) {
              item.actionC.onclick =
                item.actionC.onclick ||
                (e => {
                  if (item.pAid < 0) return;

                  const user = data.users[item.pAid];

                  if (e.ctrlKey) {
                    editUser(item.pAid, item.pName, item.reFilter);
                  } else {
                    if (user) {
                      if (user.tags.length) {
                        user.enabled = !user.enabled;
                        user.name = item.pName;
                      } else {
                        delete data.users[user.id];
                      }
                    } else {
                      addUser(item.pAid, item.pName);
                    }

                    saveData();
                    item.reFilter();
                  }
                });
            }

            item.reFilter();
          });
        };
      } else if (uPage) {
        return () => {
          const container = document.getElementById("ucp_block");

          if (container.firstChild) {
            const uid = container.innerText.match(/用户ID\s*:\s*(\S+)/)[1];

            const name = container.innerText.match(/用户名\s*:\s*(\S+)/)[1];

            container.tagC =
              container.tagC ||
              (() => {
                const c = document.createElement("span");

                c.innerHTML = `
                    <h2 class="catetitle">:: ${name} 的标记 ::</h2>
                    <div class="cateblock" style="text-align: left; line-height: 1.8em;">
                        <div class="contentBlock" style="padding: 5px 10px;">
                            <span>
                                <ul class="actions" style="padding: 0px; margin: 0px;">
                                    <li style="padding-right: 5px;">
                                        <span>
                                            <a href="javascript: void(0);">[编辑 ${name} 的标记]</a>
                                        </span>
                                    </li>
                                    <div class="clear"></div>
                                </ul>
                            </span>
                            <div class="filter-tags"></div>
                            <div class="clear"></div>
                        </div>
                    </div>
                `;

                c.getElementsByTagName("a")[0].onclick = () => {
                  editUser(uid, name, container.refresh);
                };

                container.firstChild.insertBefore(
                  c,
                  container.firstChild.childNodes[1]
                );

                return c.getElementsByClassName("filter-tags")[0];
              })();

            container.refresh = () => {
              container.tagC.innerHTML = data.users[uid].tags
                .map(
                  tag =>
                    `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`
                )
                .join("");
            };

            container.refresh();
          }
        };
      }

      return () => {};
    })();

    const observer = new MutationObserver(mutations => {
      if (mutations.find(mutation => mutation.addedNodes.length)) {
        func();
      }
    });

    if (tPage) {
      observer.observe(document.getElementById("topicrows"), {
        childList: true
      });
    } else if (pPage) {
      observer.observe(document.getElementById("m_posts_c"), {
        childList: true
      });
    } else if (uPage) {
      observer.observe(document.getElementById("ucp_block"), {
        childList: true
      });
    }

    func();

    return func;
  })();

  // STYLE
  GM_addStyle(`
    .filter-tags {
        margin: 2px -0.2em 0;
        text-align: left;
    }
    .filter-table {
        border: 1px solid #ead5bc;
        border-left: none;
        border-bottom: none;
        width: 99.95%;
    }
    .filter-table thead {
        background-color: #591804;
        color: #fff8e7;
    }
    .filter-table tbody tr {
        background-color: #fff0cd;
    }
    .filter-table tbody tr:nth-of-type(odd) {
        background-color: #fff8e7;
    }
    .filter-table td {
        border: 1px solid #ead5bc;
        border-top: none;
        border-right: none;
        padding: 6px;
    }
  `);

  // UI
  const u = (() => {
    const modules = {};

    const tabContainer = (() => {
      const c = document.createElement("div");

      c.className = "w100";
      c.innerHTML = `
          <div class="right_" style="margin-bottom: 5px;">
              <table class="stdbtn" cellspacing="0">
                  <tbody>
                      <tr></tr>
                  </tbody>
              </table>
          </div>
          <div class="clear"></div>
          `;

      return c;
    })();

    const tabPanelContainer = (() => {
      const c = document.createElement("div");

      c.style =
        "min-width: 20vw; max-width: 80vw; max-height: 80vh; overflow: auto;";
      c.innerHTML = `
            `;

      return c;
    })();

    const content = (() => {
      const c = document.createElement("div");

      c.append(tabContainer);
      c.append(tabPanelContainer);

      return c;
    })();

    const addModule = (() => {
      const tc = tabContainer.getElementsByTagName("tr")[0];
      const cc = tabPanelContainer;

      return module => {
        const tabBox = document.createElement("td");

        tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;

        const tab = tabBox.childNodes[0];

        const toggle = () => {
          Object.values(modules).forEach(item => {
            if (item.tab === tab) {
              item.tab.className = "nobr";
              item.content.style = "display: block";
              item.refresh();
            } else {
              item.tab.className = "nobr silver";
              item.content.style = "display: none";
            }
          });
        };

        tc.append(tabBox);
        cc.append(module.content);

        tab.onclick = toggle;

        modules[module.name] = {
          ...module,
          tab,
          toggle
        };

        return modules[module.name];
      };
    })();

    return {
      content,
      modules,
      addModule
    };
  })();

  // 屏蔽列表
  const blockModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <table class="filter-table">
            <thead>
                <tr>
                    <td>
                        <b style="margin: 0.1em 0.2em;">昵称</b>
                    </td>
                    <td>
                        <b style="margin: 0.1em 0.2em;">标记</b>
                    </td>
                    <td>
                        <b style="margin: 0.1em 0.2em;">操作</b>
                    </td>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
            `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(data.users).forEach(item => {
          const tc = document.createElement("tr");

          tc.refresh = () => {
            if (data.users[item.id]) {
              tc.innerHTML = `
                <tr>
                    <td>
                        <a href="/nuke.php?func=ucp&uid=${
                          item.id
                        }" class="b nobr">[${
                item.name ? "@" + item.name : "#" + item.id
              }]</a>
                    </td>
                    <td>
                        ${item.tags
                          .map(tag => {
                            if (data.tags[tag]) {
                              return `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`;
                            }
                          })
                          .join("")}
                    </td>
                    <td class="nobr">
                        <button>编辑</button>
                        <button>${item.enabled ? "禁用" : "启用"}</button>
                        <button>删除</button>
                    </td>
                </tr>
                `;

              const actions = tc.getElementsByTagName("button");

              actions[0].onclick = () => {
                editUser(item.id, item.name, tc.refresh);
              };

              actions[1].onclick = () => {
                data.users[item.id].enabled = !data.users[item.id].enabled;
                actions[1].innerHTML = data.users[item.id].enabled
                  ? "禁用"
                  : "启用";

                saveData();
                reFilter();
              };

              actions[2].onclick = () => {
                if (confirm("是否确认?")) {
                  delete data.users[item.id];
                  container.removeChild(tc);

                  saveData();
                  reFilter();
                }
              };
            } else {
              tc.remove();
            }
          };

          tc.refresh();

          container.appendChild(tc);
        });
      };

      return func;
    })();

    return {
      name: "屏蔽列表",
      content,
      refresh
    };
  })();

  // 标记设置
  const tagModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";
      c.innerHTML = `
        <table class="filter-table">
            <thead>
                <tr>
                    <td>
                        <b style="margin: 0.1em 0.2em;">标记</b>
                    </td>
                    <td>
                        <b style="margin: 0.1em 0.2em;">列表</b>
                    </td>
                    <td>
                        <b style="margin: 0.1em 0.2em;">操作</b>
                    </td>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
            `;

      return c;
    })();

    const refresh = (() => {
      const container = content.getElementsByTagName("tbody")[0];

      const func = () => {
        container.innerHTML = "";

        Object.values(data.tags).forEach(item => {
          const tc = document.createElement("tr");

          tc.innerHTML = `
            <tr>
                <td>
                    <b class="block_txt nobr" style="background:${
                      item.color
                    }; color:#fff; margin: 0.1em 0.2em;">${item.name}</b>
                </td>
                <td>
                    <button>${
                      Object.values(data.users).filter(user =>
                        user.tags.find(tag => tag === item.id)
                      ).length
                    }
                    </button>
                    <div style="display: none;">
                        ${Object.values(data.users)
                          .filter(user =>
                            user.tags.find(tag => tag === item.id)
                          )
                          .map(
                            user =>
                              `<a href="/nuke.php?func=ucp&uid=${
                                user.id
                              }" class="b nobr">[${
                                user.name ? "@" + user.name : "#" + user.id
                              }]</a>`
                          )
                          .join("")}
                    </div>
                </td>
                <td class="nobr">
                    <button>${item.enabled ? "禁用" : "启用"}</button>
                    <button>删除</button>
                </td>
            </tr>
            `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = (() => {
            let hide = true;
            return () => {
              hide = !hide;
              actions[0].nextElementSibling.style = `display: ${
                hide ? "none" : "block"
              };`;
            };
          })();

          actions[1].onclick = () => {
            data.tags[item.id].enabled = !data.tags[item.id].enabled;
            actions[1].innerHTML = data.tags[item.id].enabled ? "禁用" : "启用";

            saveData();
            reFilter();
          };

          actions[2].onclick = () => {
            if (confirm("是否确认?")) {
              delete data.tags[item.id];

              Object.values(data.users).forEach(user => {
                const index = user.tags.findIndex(tag => tag === item.id);
                if (index >= 0) {
                  user.tags.splice(index, 1);
                }
              });

              container.removeChild(tc);

              saveData();
              reFilter();
            }
          };

          container.appendChild(tc);
        });
      };

      return func;
    })();

    return {
      name: "标记设置",
      content,
      refresh
    };
  })();

  // 通用设置
  const commonModule = (() => {
    const content = (() => {
      const c = document.createElement("div");

      c.style = "display: none";

      return c;
    })();

    const refresh = (() => {
      const container = content;

      const func = () => {
        container.innerHTML = "";

        // 屏蔽关键词
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <div>过滤关键词,用"|"隔开</div>
            <div>
                <input value="${data.options.keyword}"/>
                <button>确认</button>
            </div>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            const v = actions[0].previousElementSibling.value;

            data.options.keyword = v;

            saveData();
            reFilter();
          };

          container.appendChild(tc);
        }

        // 过滤方式
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <br/>
            <div>过滤方式</div>
            <div>
                <input type="radio" name="filterType" ${data.options
                  .filterMode === 0 && "checked"}>
                <span>隐藏</span>
                <input type="radio" name="filterType" ${data.options
                  .filterMode === 1 && "checked"}>
                <span>标记</span>
                <button>确认</button>
            </div>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            const values = document.getElementsByName("filterType");

            for (let i = 0, length = values.length; i < length; i++) {
              if (values[i].checked) {
                data.options.filterMode = i;
                break;
              }
            }

            saveData();
            reFilter();
          };

          container.appendChild(tc);
        }

        // 删除没有标记的用户
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <br/>
            <div>
                <button>删除没有标记的用户</button>
            </div>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            if (confirm("是否确认?")) {
              Object.values(data.users).forEach(item => {
                if (item.tags.length === 0) {
                  delete data.users[item.id];
                }
              });

              saveData();
              reFilter();
            }
          };

          container.appendChild(tc);
        }

        // 删除没有用户的标记
        {
          const tc = document.createElement("div");

          tc.innerHTML += `
            <br/>
            <div>
                <button>删除没有用户的标记</button>
            </div>
          `;

          const actions = tc.getElementsByTagName("button");

          actions[0].onclick = () => {
            if (confirm("是否确认?")) {
              Object.values(data.tags).forEach(item => {
                if (
                  Object.values(data.users).filter(user =>
                    user.tags.find(tag => tag === item.id)
                  ).length === 0
                ) {
                  delete data.tags[item.id];
                }
              });

              saveData();
              reFilter();
            }
          };

          container.appendChild(tc);
        }
      };

      return func;
    })();

    return {
      name: "通用设置",
      content,
      refresh
    };
  })();

  u.addModule(blockModule).toggle();
  u.addModule(tagModule);
  u.addModule(commonModule);

  // 增加菜单项
  (() => {
    const title = "屏蔽/标记";
    let window;

    n.mainMenu.addItemOnTheFly(title, null, () => {
      if (window === undefined) {
        window = n.createCommmonWindow();
      }

      window._.addContent(null);
      window._.addTitle(title);
      window._.addContent(u.content);
      window._.show();
    });
  })();
})(commonui, __CURRENT_UID);