NGA Follow Support

同步客户端关注功能

As of 01.04.2024. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        NGA Follow Support
// @namespace   https://greatest.deepsurf.us/users/263018
// @version     1.3.5
// @author      snyssss
// @description 同步客户端关注功能

// @match       *://bbs.nga.cn/*
// @match       *://ngabbs.com/*
// @match       *://nga.178.com/*

// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_addValueChangeListener
// @grant       GM_registerMenuCommand

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

((ui, self) => {
  if (!ui || !self) return;

  // 钩子
  const hookFunction = (object, functionName, callback) => {
    ((originalFunction) => {
      object[functionName] = function () {
        const returnValue = originalFunction.apply(this, arguments);

        callback.apply(this, [returnValue, originalFunction, arguments]);

        return returnValue;
      };
    })(object[functionName]);
  };

  ui.sn = ui.sn || {};
  ui.sn.userInfo = ui.sn.userInfo || {};

  ((info) => {
    // 用户信息
    class UserInfo {
      execute(task) {
        task().finally(() => {
          if (this.waitingQueue.length) {
            const next = this.waitingQueue.shift();

            this.execute(next);
          } else {
            this.isRunning = false;
          }
        });
      }

      enqueue(task) {
        if (this.isRunning) {
          this.waitingQueue.push(task);
        } else {
          this.isRunning = true;

          this.execute(task);
        }
      }

      rearrange() {
        if (this.data) {
          const list = Object.values(this.children);

          for (let i = 0; i < list.length; i++) {
            if (list[i].source === undefined) {
              list[i].create(this.data);
            }

            Object.entries(this.container).forEach((item) => {
              list[i].clone(this.data, item);
            });
          }
        }
      }

      reload() {
        this.enqueue(async () => {
          this.data = await get_user_info(this.uid);

          Object.values(this.children).forEach((item) => item.destroy());

          this.rearrange();
        });
      }

      constructor(id) {
        this.uid = id;

        this.waitingQueue = [];
        this.isRunning = false;

        this.container = {};
        this.children = {};

        this.reload();
      }
    }

    // 用户信息组件
    class UserInfoWidget {
      destroy() {
        if (this.source) {
          this.source = undefined;
        }

        if (this.target) {
          Object.values(this.target).forEach((item) => {
            if (item.parentNode) {
              item.parentNode.removeChild(item);
            }
          });
        }
      }

      clone(data, [argid, container]) {
        if (this.source) {
          if (this.target[argid] === undefined) {
            this.target[argid] = this.source.cloneNode(true);

            if (this.callback) {
              this.callback(data, this.target[argid]);
            }
          }

          const isSmall = container.classList.contains("posterInfoLine");

          if (isSmall) {
            const anchor = container.querySelector(".author ~ br");

            if (anchor) {
              anchor.parentNode.insertBefore(this.target[argid], anchor);
            }
          } else {
            container.appendChild(this.target[argid]);
          }
        }
      }

      constructor(func, callback) {
        this.create = (data) => {
          this.destroy();

          this.source = func(data);
          this.target = {};
        };

        this.callback = callback;
      }
    }

    // 扩展规则
    const extraData = (() => {
      const key = `EXTRA_DATA`;
      const data = GM_getValue(key) || {
        [0]: {
          time: 0,
        },
      };

      const save = () => {
        GM_setValue(key, data);
      };

      const setValue = (uid, value) => {
        data[uid] = value;

        save();
      };

      const getValue = (uid) => data[uid];

      const remove = (uid) => {
        delete data[uid];

        save();
      };

      const specialList = () => {
        const result = Object.entries(data).filter(
          ([key, value]) => value.level
        );

        if (Object.keys(result)) {
          return result;
        }

        return null;
      };

      GM_addValueChangeListener(key, function (_, prev, next) {
        Object.assign(data, next);
      });

      return {
        specialList,
        setValue,
        getValue,
        remove,
      };
    })();

    // 获取用户信息
    const get_user_info = (uid) => {
      const searchPair = (content, keyword, start = "{", end = "}") => {
        // 获取成对括号的位置
        const getLastIndex = (content, position, start = "{", end = "}") => {
          if (position >= 0) {
            let nextIndex = position + 1;

            while (nextIndex < content.length) {
              if (content[nextIndex] === end) {
                return nextIndex;
              }

              if (content[nextIndex] === start) {
                nextIndex = getLastIndex(content, nextIndex, start, end);

                if (nextIndex < 0) {
                  break;
                }
              }

              nextIndex = nextIndex + 1;
            }
          }

          return -1;
        };

        // 起始位置
        const str = keyword + start;

        // 起始下标
        const index = content.indexOf(str) + str.length;

        // 结尾下标
        const lastIndex = getLastIndex(content, index, start, end);

        if (lastIndex >= 0) {
          return start + content.substring(index, lastIndex) + end;
        }

        return null;
      };

      return new Promise((resolve, reject) => {
        fetch(`/nuke.php?func=ucp&uid=${uid}`)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = searchPair(reader.result, `__UCPUSER =`);

              if (text) {
                try {
                  resolve(JSON.parse(text));
                  return;
                } catch {
                  reject();
                  return;
                }
              }

              reject();
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject();
          });
      });
    };

    // 获取用户发帖列表
    const get_user_topic_list = (uid) =>
      new Promise((resolve) => {
        fetch(`/thread.php?lite=js&authorid=${uid}`)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data.__T);
              } else {
                resolve({});
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve({});
          });
      });

    // 获取用户回帖列表
    const get_user_post_list = (uid) =>
      new Promise((resolve, reject) => {
        fetch(`/thread.php?lite=js&authorid=${uid}&searchpost=1`)
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data.__T);
              } else {
                resolve({});
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve({});
          });
      });

    // 关注
    const follow = (uid) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=1`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject();
          });
      });

    // 取消关注
    const un_follow = (uid) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=8`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject();
          });
      });

    // 移除粉丝
    const un_follow_fans = (uid) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=256`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject();
          });
      });

    // 获取关注列表
    const follow_list = (page) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow&page=${page}`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject();
          });
      });

    // 获取粉丝列表
    const follow_by_list = (page) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow_by&page=${page}`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject();
          });
      });

    // 获取关注动态
    const follow_dymanic_list = (page) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=get_push_list&page=${page}`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = eval(`
                (${text.replace("window.script_muti_get_var_store=", "")})
              `);

              if (result.data) {
                resolve(result.data);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            reject();
          });
      });

    // 切换关注
    const handleSwitchFollow = (uid, isFollow) => {
      if (isFollow) {
        if (confirm("取消关注?")) {
          un_follow(uid).then(() => {
            info[uid]?.reload();
            u.refresh();
          });
        }
      } else {
        follow(uid).then(() => {
          info[uid]?.reload();
          u.refresh();
        });
      }
    };

    // 移除粉丝
    const handleRemoveFans = (uid) => {
      if (confirm("移除粉丝?")) {
        un_follow_fans(uid).then(() => {
          u.refresh();
        });
      }
    };

    // STYLE
    GM_addStyle(`
      .s-user-info-container:not(:hover) .ah {
        display: none !important;
      }
      .s-table-wrapper {
        height: calc((2em + 10px) * 11 + 3px);
        overflow-y: auto;
      }
      .s-table {
        margin: 0;
      }
      .s-table th,
      .s-table td {
        position: relative;
        white-space: nowrap;
      }
      .s-table th {
        position: sticky;
        top: 2px;
        z-index: 1;
      }
      .s-table input:not([type]), .s-table input[type="text"] {
        margin: 0;
        box-sizing: border-box;
        height: 100%;
        width: 100%;
      }
      .s-input-wrapper {
        position: absolute;
        top: 6px;
        right: 6px;
        bottom: 6px;
        left: 6px;
      }
      .s-text-ellipsis {
        display: flex;
      }
      .s-text-ellipsis > * {
        flex: 1;
        width: 1px;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      .s-button-group {
        margin: -.1em -.2em;
      }
    `);

    // MENU
    const m = (() => {
      const container = document.createElement("DIV");

      container.className = `td`;
      container.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">关注</a>`;

      const content = container.querySelector("A");

      const create = (onclick) => {
        const anchor = document.querySelector("#mainmenu .td:last-child");

        anchor.before(container);

        content.onclick = onclick;
      };

      const update = (count) => {
        if (count) {
          content.innerHTML = `关注 <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
        } else {
          content.innerHTML = `关注`;
        }
      };

      return {
        create,
        update,
      };
    })();

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

      const createView = () => {
        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 = "width: 800px;";

          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.visible = true;
                } else {
                  item.tab.className = "nobr silver";
                  item.content.style = "display: none";
                  item.visible = false;
                }
              });

              module.refresh();
            };

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

            tab.onclick = (() => {
              let timeout;

              return () => {
                if (timeout > 0) {
                  return;
                }

                timeout = setTimeout(() => {
                  timeout = 0;
                }, 320);

                toggle();
              };
            })();

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

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

        return {
          content,
          addModule,
        };
      };

      const refresh = () => {
        Object.values(modules)
          .find((item) => item.visible)
          ?.refresh();
      };

      return {
        modules,
        createView,
        refresh,
      };
    })();

    // 我的关注
    {
      const name = "我的关注";

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

        c.style.display = "none";
        c.innerHTML = `
        <div class="s-table-wrapper">
          <table class="s-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1" width="1">用户</th>
                <th class="c2">过滤规则</th>
                <th class="c3" width="1">特别关注</th>
                <th class="c4" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        <div class="silver" style="margin-top: 5px;">特别关注功能需要占用额外的资源,请谨慎开启</div>
        `;

        return c;
      })();

      let page = 0;
      let hasNext = false;
      let isFetching = false;

      const box = content.querySelector("DIV");

      const list = content.querySelector("TBODY");

      const wrapper = content.querySelector(".s-table-wrapper");

      const fetchData = () => {
        isFetching = true;

        follow_list(page)
          .then((res) => {
            hasNext = Object.keys(res).length > 0;

            for (let i in res) {
              const { uid, username } = res[i];

              const data = extraData.getValue(uid) || {};

              if (list.querySelector(`[data-id="${uid}"]`)) {
                continue;
              }

              const item = document.createElement("TR");

              item.className = `row${
                (list.querySelectorAll("TR").length % 2) + 1
              }`;

              item.setAttribute("data-id", uid);

              item.innerHTML = `
                <td class="c1">
                  <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">${username}</a>
                </td>
                <td class="c2">
                  <div class="s-input-wrapper">
                    <input value="${data.rule || ""}" />
                  </div>
                </td>
                <td class="c3">
                  <div style="text-align: center;">
                    <input type="checkbox" ${
                      data.level ? `checked="checked"` : ""
                    } />
                  </div>
                </td>
                <td class="c4">
                  <div class="s-button-group">
                    <button>重置</button>
                    <button>移除</button>
                  </div>
                </td>
              `;

              const ruleElement = item.querySelector("INPUT");
              const levelElement = item.querySelector(`INPUT[type="checkbox"]`);
              const actions = item.querySelectorAll("BUTTON");

              const save = () => {
                extraData.setValue(uid, {
                  rule: ruleElement.value,
                  level: levelElement.checked ? 1 : 0,
                });
              };

              const clear = () => {
                ruleElement.value = "";
                levelElement.checked = false;

                save();
              };

              ruleElement.onchange = save;

              levelElement.onchange = save;

              actions[0].onclick = () => clear();
              actions[1].onclick = () => handleSwitchFollow(uid, 1);

              list.appendChild(item);
            }
          })
          .finally(() => {
            isFetching = false;
          });
      };

      box.onscroll = () => {
        if (isFetching || !hasNext) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          page = page + 1;

          fetchData();
        }
      };

      const refresh = () => {
        list.innerHTML = "";

        page = 1;
        hasNext = false;

        fetchData();
      };

      hookFunction(u, "createView", (view) => {
        view.addModule({
          name,
          content,
          refresh,
        });
      });
    }

    // 我的粉丝
    {
      const name = "我的粉丝";

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

        c.style.display = "none";
        c.innerHTML = `
        <div class="s-table-wrapper">
          <table class="s-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1">用户</th>
                <th class="c2" width="1">操作</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        `;

        return c;
      })();

      let page = 0;
      let hasNext = false;
      let isFetching = false;

      const box = content.querySelector("DIV");

      const list = content.querySelector("TBODY");

      const wrapper = content.querySelector(".s-table-wrapper");

      const fetchData = () => {
        isFetching = true;

        follow_by_list(page)
          .then((res) => {
            hasNext = Object.keys(res).length > 0;

            for (let i in res) {
              const { uid, username } = res[i];

              if (list.querySelector(`[data-id="${uid}"]`)) {
                continue;
              }

              const item = document.createElement("TR");

              item.className = `row${
                (list.querySelectorAll("TR").length % 2) + 1
              }`;

              item.setAttribute("data-id", uid);

              item.innerHTML = `
                <td class="c1">
                  <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">${username}</a>
                </td>
                <td class="c2">
                  <div class="s-button-group">
                    <button>移除</button>
                  </div>
                </td>
              `;

              const action = item.querySelector("BUTTON");

              action.onclick = () => handleRemoveFans(uid);

              list.appendChild(item);
            }
          })
          .finally(() => {
            isFetching = false;
          });
      };

      box.onscroll = () => {
        if (isFetching || !hasNext) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          page = page + 1;

          fetchData();
        }
      };

      const refresh = () => {
        list.innerHTML = "";

        page = 1;
        hasNext = false;

        fetchData();
      };

      hookFunction(u, "createView", (view) => {
        view.addModule({
          name,
          content,
          refresh,
        });
      });
    }

    // 关注动态
    {
      const name = "关注动态";

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

        c.style.display = "none";
        c.innerHTML = `
        <div class="s-table-wrapper">
          <table class="s-table forumbox">
            <thead>
              <tr class="block_txt_c0">
                <th class="c1" width="1">时间</th>
                <th class="c2">内容</th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
        `;

        return c;
      })();

      let page = 0;
      let hasNext = false;
      let isFetching = false;

      const box = content.querySelector("DIV");

      const list = content.querySelector("TBODY");

      const wrapper = content.querySelector(".s-table-wrapper");

      const fetchData = () => {
        isFetching = true;

        follow_dymanic_list(page)
          .then((res) =>
            Promise.all(
              Object.keys(res[1]).map((uid) =>
                get_user_info(uid).then((item) => {
                  if (item.follow) {
                    const info = extraData.getValue(uid) || {
                      rule: "",
                      level: 0,
                    };

                    extraData.setValue(uid, {
                      ...info,
                    });
                  } else {
                    extraData.remove(uid);
                  }
                })
              )
            ).then(() => {
              return res;
            })
          )
          .then((res) => {
            hasNext = res[2] > res[3];

            extraData.setValue(0, {
              time: Math.floor(new Date() / 1000),
              unread: 0,
            });

            return res;
          })
          .then((res) => {
            const filtered = Object.values(res[0])
              .map((item) => ({
                id: item[0],
                uid: item[2],
                info: item[4]
                  ? res[4][`${item[3]}_${item[4]}`]
                  : res[4][item[3]],
                time: item[6],
                summary: item.summary
                  .replace(
                    /\[uid=(\d+)\](.+)\[\/uid\]/,
                    `<a href="/nuke.php?func=ucp&uid=${item[2]}" class="b nobr">$2</a>`
                  )
                  .replace(
                    /\[pid=(\d+)\](.+)\[\/pid\](\s?)/,
                    `<a href="/read.php?pid=${item[4]}" class="b nobr">回复</a>`
                  )
                  .replace(
                    /\[tid=(\d+)\](.+)\[\/tid\]/,
                    item[4] === 0
                      ? `<a href="/read.php?tid=${item[3]}" title="$2" class="b nobr">$2</a>`
                      : `<a href="/read.php?pid=${item[4]}&opt=128" title="$2" class="b nobr">$2</a>`
                  ),
              }))
              .filter((item) => {
                const { uid, info } = item;

                const data = extraData.getValue(uid);

                if (data) {
                  const { rule } = data;

                  if (rule) {
                    return (
                      info.subject.search(rule) >= 0 ||
                      info.content.search(rule) >= 0
                    );
                  }

                  return true;
                }

                return false;
              });

            return filtered;
          })
          .then((res) => {
            for (let i in res) {
              const { id, time, summary } = res[i];

              if (list.querySelector(`[data-id="${id}"]`)) {
                continue;
              }

              const item = document.createElement("TR");

              item.className = `row${
                (list.querySelectorAll("TR").length % 2) + 1
              }`;

              item.setAttribute("data-id", id);
              item.setAttribute("data-time", time);

              item.innerHTML = `
                <td class="c1">
                  <span class="nobr">${ui.time2dis(time)}</span>
                </td>
                <td class="c2">
                  <div class="s-text-ellipsis">
                    <span>${summary}</span>
                  </div>
                </td>
              `;

              list.appendChild(item);
            }

            if (box.scrollHeight === box.clientHeight && hasNext) {
              page = page + 1;

              fetchData();
            }
          })
          .finally(() => {
            isFetching = false;
          });
      };

      box.onscroll = () => {
        if (isFetching || !hasNext) {
          return;
        }

        if (
          box.scrollHeight - box.scrollTop - box.clientHeight <=
          wrapper.clientHeight
        ) {
          page = page + 1;

          fetchData();
        }
      };

      const refresh = () => {
        list.innerHTML = "";

        page = 1;
        hasNext = false;

        fetchData();
      };

      hookFunction(u, "createView", (view) => {
        view.addModule({
          name,
          content,
          refresh,
        });
      });
    }

    // 打开菜单
    const handleCreateView = (() => {
      let view, window;

      return () => {
        if (view === undefined) {
          view = u.createView();
        }

        u.modules["关注动态"].toggle();
        m.update(0);

        if (window === undefined) {
          window = ui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(`关注`);
        window._.addContent(view.content);
        window._.show();
      };
    })();

    // 扩展用户信息
    (() => {
      const execute = (argid) => {
        const args = ui.postArg.data[argid];

        if (args.comment) return;

        const uid = +args.pAid;

        if (uid > 0) {
          if (info[uid] === undefined) {
            info[uid] = new UserInfo(uid);
          }

          if (document.contains(info[uid].container[argid]) === false) {
            info[uid].container[argid] =
              args.uInfoC.closest("tr").querySelector(".posterInfoLine") ||
              args.uInfoC.querySelector("div");
          }

          info[uid].enqueue(async () => {
            args.uInfoC.className =
              args.uInfoC.className + " s-user-info-container";

            if (info[uid].children[16]) {
              info[uid].children[16].destroy();
            }

            info[uid].children[16] = new UserInfoWidget(
              (data) => {
                const value = data.follow_by_num || 0;

                const element = document.createElement("SPAN");

                if (uid === self || data.follow) {
                  element.className =
                    "small_colored_text_btn stxt block_txt_c2 vertmod";
                } else {
                  element.className =
                    "small_colored_text_btn stxt block_txt_c2 vertmod ah";
                }

                element.style.cursor = "default";
                element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span>&nbsp;${value}</span>`;

                element.style.cursor = "pointer";

                return element;
              },
              (data, element) => {
                element.onclick = () => {
                  if (data.uid === self) {
                    handleCreateView();
                  } else {
                    handleSwitchFollow(data.uid, data.follow);
                  }
                };
              }
            );

            info[uid].rearrange();
          });
        }
      };

      let initialized = false;

      if (ui.postArg) {
        Object.keys(ui.postArg.data).forEach((i) => execute(i));
      }

      hookFunction(ui, "eval", () => {
        if (initialized) return;

        if (ui.postDisp) {
          hookFunction(
            ui,
            "postDisp",
            (returnValue, originalFunction, arguments) => execute(arguments[0])
          );

          initialized = true;
        }
      });
    })();

    // 提醒关注
    (async () => {
      // 增加菜单项
      m.create(handleCreateView);

      // 获取动态
      (() => {
        const cache = extraData.getValue(0) || {
          time: 0,
          unread: 0,
        };

        const fetchData = async (page = 1, result = {}) =>
          new Promise((resolve) => {
            follow_dymanic_list(page).then(async (res) => {
              const list = Object.values(res[0]);
              const prefiltered = list
                .map((item) => ({
                  id: item[0],
                  uid: item[2],
                  info: item[4]
                    ? res[4][`${item[3]}_${item[4]}`]
                    : res[4][item[3]],
                  time: item[6],
                }))
                .filter((item) => item.time > (cache.time || 0))
                .filter((item) => {
                  if (result[item.id]) {
                    return false;
                  }

                  result[item.id] = item;
                  return true;
                });

              if (prefiltered.length) {
                await Promise.all(
                  Object.keys(res[1]).map((uid) =>
                    get_user_info(uid).then((item) => {
                      if (item.follow) {
                        const info = extraData.getValue(uid) || {
                          rule: "",
                          level: 0,
                        };

                        extraData.setValue(uid, {
                          ...info,
                        });
                      } else {
                        extraData.remove(uid);
                      }
                    })
                  )
                );

                const hasNext =
                  prefiltered.length === list.length && res[2] > res[3];

                if (hasNext) {
                  const withNext = await fetchData(page + 1, result);

                  resolve(withNext);
                }
              }

              resolve(result);
            });
          });

        fetchData().then((res) => {
          const filtered = Object.values(res).filter((item) => {
            const { uid, info } = item;

            const data = extraData.getValue(uid);

            if (data) {
              const { rule } = data;

              if (rule) {
                return (
                  info.subject.search(rule) >= 0 ||
                  info.content.search(rule) >= 0
                );
              }

              return true;
            }

            return false;
          });

          const unread = (cache.unread || 0) + filtered.length;

          extraData.setValue(0, {
            time: Math.floor(new Date() / 1000),
            unread: unread,
          });

          m.update(unread);
        });
      })();

      // 特别关注
      {
        const fetchData = async (uid, value) => {
          // 请求用户信息
          const { username, follow, posts } = await get_user_info(uid);

          // 用户缓存
          const { rule, time, postNum } = value;

          // 已取消关注
          if (follow === 0) {
            extraData.remove(uid);
            return [];
          }

          // 判断是否有新活动
          if (posts <= (postNum || 0)) {
            return [];
          }

          // 是否匹配
          const isMatch = (text) => {
            if (rule) {
              return text.search(rule) >= 0;
            }

            return true;
          };

          // 请求发帖记录
          const ts = await get_user_topic_list(uid).then((res) =>
            Object.values(res)
              .filter(
                (item) => item.postdate > (time || 0) && isMatch(item.subject)
              )
              .map((item) => ({
                0: 5,
                1: item.authorid,
                2: item.author,
                5: item.subject,
                6: item.tid,
                9: item.postdate,
                10: 1,
              }))
          );

          // 请求回帖记录
          const ps = await get_user_post_list(uid).then((res) =>
            Object.values(res)
              .filter(
                (item) =>
                  item.__P.postdate > (time || 0) && isMatch(item.__P.content)
              )
              .map((item) => ({
                0: 6,
                1: uid,
                2: username,
                5: item.subject,
                6: item.__P.tid,
                7: item.__P.pid,
                9: item.__P.postdate,
                10: 1,
              }))
          );

          // 更新缓存
          extraData.setValue(uid, {
            ...value,
            time: Math.floor(new Date() / 1000),
            postNum: posts,
          });

          // 返回结果
          return [...ts, ...ps];
        };

        const data = (
          await Promise.all(
            extraData
              .specialList()
              .map(async ([key, value]) => await fetchData(key, value))
          )
        )
          .flat()
          .sort((a, b) => a[9] - b[9]);

        if (Object.keys(data).length) {
          const func = () => {
            // 修复 NGA 脚本错误
            TPL[KEY["_BIT_SYS"]][KEY["_TYPE_KEYWORD_WATCH_REPLY"]] = function (
              x
            ) {
              return x[KEY["_ABOUT_ID_4"]]
                ? "{_U} 在{_T1} {_R2} 中的 {_R5} 触发了关键词监视<br/>"
                : "{_U} 在主题 {_T} 中的 {_R5} 触发了关键词监视<br/>";
            };

            // 推送消息
            for (let i in data) {
              ui.notification._add(1, data[i], 1);
            }

            // 打开窗口
            ui.notification.openBox();
          };

          if (ui.notification) {
            func();
          } else {
            ui.loadNotiScript(() => {
              func();
            });
          }
        }
      }
    })();
  })(ui.sn.userInfo);
})(commonui, __CURRENT_UID);