剧本杀活动通知生成器

用于获取本周剧本杀活动信息并生成 Markdown 代码

目前为 2024-02-09 提交的版本。查看 最新版本

// ==UserScript==
// @name         剧本杀活动通知生成器
// @namespace    https://github.com/heiyexing
// @version      2024-02-10
// @description  用于获取本周剧本杀活动信息并生成 Markdown 代码
// @author       炎熊
// @match        https://yuque.antfin-inc.com/yuhmb7/pksdw8/**
// @match        https://yuque.antfin.com/yuhmb7/pksdw8/**
// @icon         https://www.google.com/s2/favicons?sz=64&domain=antfin-inc.com
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/dayjs.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/plugin/isSameOrAfter.js
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/plugin/isSameOrBefore.js
// @require      https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/locale/zh-cn.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/layui/2.8.17/layui.min.js
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const BTN_CLASS_NAME = "murder-mystery-btn";
  const USER_LIST_CLASS_NAME = "murder-user-list";
  const USER_ITEM_CLASS_NAME = "murder-user-item";

  dayjs.locale(dayjs_locale_zh_cn);
  dayjs.extend(dayjs_plugin_isSameOrAfter);
  dayjs.extend(dayjs_plugin_isSameOrBefore);

  function initStyle() {
    const style = document.createElement("style");
    style.innerHTML = `
          .${BTN_CLASS_NAME} {
              position: fixed;
              bottom: 25px;
              right: 80px;
              width: 40px;
              height: 40px;
              background-color: #fff;
              border-radius: 50%;
              box-shadow: 0 0 10px rgba(0, 0, 0, .2);
              cursor: pointer;
              display: inline-flex;
              justify-content: center;
              align-items: center;
              z-index: 2;
          }
          .${BTN_CLASS_NAME} img {
              width: 20px;
          }
          .${USER_LIST_CLASS_NAME} {
            display: flex;
            flex-wrap: wrap;
          }
          .${USER_ITEM_CLASS_NAME} {
            margin-right: 12px;
            margin-bottom: 12px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            line-height: 14px;
            border-radius: 6px;
            padding: 6px;
            border: 1px solid #E7E9E8;
          }
          .${USER_ITEM_CLASS_NAME}.unchecked {
            border-color: #ff0000;
          }
          .${USER_ITEM_CLASS_NAME} span {
            white-space: nowrap;
          }
          .${USER_ITEM_CLASS_NAME} img {
            width: 30px;
            height: 30px;
            border-radius: 30px;
            margin-right: 6px;
          }
          .layui-card-body {
            width: 100%;
          }
          .layui-card-footer {
            display: flex;
            justify-content: space-between;
            align-items: center;
          }
          `;
    const link = document.createElement("link");
    link.setAttribute("rel", "stylesheet");
    link.setAttribute("type", "text/css");
    link.href =
      "https://cdn.bootcdn.net/ajax/libs/layui/2.8.17/css/layui.min.css";
    document.head.appendChild(style);
    document.head.appendChild(link);
    return style;
  }

  function initBtn() {
    const btn = document.createElement("div");
    btn.className = BTN_CLASS_NAME;
    const logo = document.createElement("img");
    logo.src =
      "https://mdn.alipayobjects.com/huamei_baaa7a/afts/img/A*f8MvQYdbHPoAAAAAAAAAAAAADqSCAQ/original";
    btn.appendChild(logo);
    document.body.appendChild(btn);
    return btn;
  }

  function getTitleInfo(title) {
    const month = title.match(/\d+(?=\s*月)/)?.[0];
    const date = title.match(/\d+(?=\s*日)/)?.[0];
    const name = title.match(/(?<=《).*?(?=》)/)?.[0];
    if (!month || !date || !name) {
      return null;
    }
    return {
      month: +month,
      date: +date,
      name,
    };
  }

  function getRegExpStr(strList, regexp) {
    for (const str of strList) {
      const result = str.match(regexp);
      if (result) {
        return result[0].trim();
      }
    }
    return "";
  }

  function exeCommandCopyText(text) {
    try {
      const t = document.createElement("textarea");
      t.nodeValue = text;
      t.value = text;
      document.body.appendChild(t);
      t.select();
      document.execCommand("copy");
      document.body.removeChild(t);
      return true;
    } catch (e) {
      console.log(e);
      return false;
    }
  }

  function getInnerText(content) {
    const div = document.createElement("div");
    div.style = "height: 0px; overflow: hidden;";
    div.innerHTML = content;
    document.body.appendChild(div);
    return div.innerText;
  }

  async function getActivesInfo(start, end) {
    if (!window.appData || !Array.isArray(window.appData?.book.toc)) {
      return;
    }
    const tocList = window.appData?.book.toc;
    const pathList = location.pathname.split("/");
    if (pathList.length <= 0) {
      return;
    }
    const docUrl = pathList[pathList.length - 1];
    const currentToc = tocList.find((item) => item.url === docUrl);
    if (!currentToc) {
      return;
    }
    const parentToc = tocList.find(
      (item) => item.uuid === currentToc.parent_uuid
    );
    if (!parentToc) {
      return;
    }
    const targetTocList = tocList.filter(
      (item) => item.parent_uuid === parentToc.uuid
    );

    const targetTimeRangeList = targetTocList
      .map((item) => {
        const titleInfo = getTitleInfo(item.title);
        if (!titleInfo) {
          return item;
        }
        return {
          ...item,
          ...titleInfo,
          dayjs: dayjs()
            .set("month", titleInfo.month - 1)
            .set("date", titleInfo.date),
        };
      })
      .filter((item) => {
        return (
          item.dayjs.isSameOrAfter(start, "date") &&
          item.dayjs.isSameOrBefore(end, "date")
        );
      })
      .sort((a, b) => a.dayjs - b.dayjs);

    return await Promise.all(
      targetTimeRangeList.map((item) => {
        return fetch(
          `${location.origin}/api/docs/${item.url}?book_id=${window.appData?.book.id}&include_contributors=true&include_like=true&include_hits=true&merge_dynamic_data=false`
        )
          .then((res) => res.json())
          .then((res) => {
            const rowList = getInnerText(res.data.content).split("\n");

            const tag = getRegExpStr(rowList, /(?<=类型\s*[::]\s*).+/)
              ?.split(/[/||]/)
              .join("/");

            const level = getRegExpStr(
              rowList,
              /(?<=(难度|适合)\s*[::\s*]).+/
            );

            const dm = getRegExpStr(rowList, /(?<=(dm|DM)\s*[::]\s*).+/);

            let place = getRegExpStr(rowList, /(?<=(地点|场地)\s*[::]\s*).+/);

            if (/[Aa]\s?空间/.test(place)) {
              place = "A空间";
            }
            if (/元空间/.test(place)) {
              place = "元空间";
            }

            const persons = getRegExpStr(rowList, /(?<=(人数)\s*[::]\s*).+/)
              .split(/[,,\(\)()「」]/)
              .map((item) => item.replace(/(回复报名|注明男女|及人数)/, ""))
              .filter((item) => item.trim())
              .join("·");

            const manCount = +persons.match(/(\d+)\s?男/)?.[1] || undefined;
            const womanCount = +persons.match(/(\d+)\s?女/)?.[1] || undefined;
            const personCount = (() => {
              if (manCount && womanCount) {
                return manCount + womanCount;
              }
              if (/(\d+)[~~到-](\d+)/.test(persons.replace(/\s/g, ""))) {
                return +/(\d+)[~~到-](\d+)/.exec(
                  persons.replaceAll(" ", "")
                )[1];
              }
              if (/(\d+)人/.test(persons.replaceAll(/\s/g, ""))) {
                return +/(\d+)人/.exec(persons.replaceAll(" ", ""))[1];
              }
              return undefined;
            })();

            const reversable = !/不[^反]*反串/.test(persons);

            const week =
              getRegExpStr(rowList, /周[一二三四五六日]/) ||
              `周${
                ["日", "一", "二", "三", "四", "五", "六"][item.dayjs.day()]
              }`;

            const time = getRegExpStr(rowList, /\d{1,2}[::]\d{2}/);

            const [hour = "", minute = ""] = time.split(/[::]/);

            const duration = getRegExpStr(
              rowList,
              /(?<=(预计时.|时长)\s*[::]\s*).+/
            ).replace(/(h|小时)/, "H");

            const url = `https://yuque.antfin.com/yuhmb7/pksdw8/${item.url}?singleDoc#`;

            return {
              ...item,
              tag,
              level,
              dm,
              week,
              hour,
              minute,
              place,
              persons,
              duration,
              url,
              manCount,
              womanCount,
              personCount,
              reversable,
            };
          });
      })
    );
  }

  async function copyMarkdownInfo(list) {
    const text = `
# 📢 剧本杀活动通知

---
${list
  .map((item) => {
    return `
🎬 《${item.name}》${item.tag}${item.level ? `/${item.level}` : ""}

🕙  ${item.month}.${item.date} ${item.week} ${item.hour}:${item.minute} 📍${
      item.place
    }

💎  DM ${item.dm}【${item.persons}·${item.duration}】[报名](${item.url})

---
`;
  })
  .join("")}
🙋‍ [玩家报名须知](https://yuque.antfin.com/yuhmb7/pksdw8/igri3gwp127v3v32?singleDoc#),防跳车押金以报名页面为准!

🔜 加入钉群:14575023754,获取更多活动信息!

`;

    exeCommandCopyText(text);
    window.layui?.layer?.msg("已复制到剪贴板");
  }

  async function getCommentsList(list) {
    return Promise.all(
      list.map((item) => {
        return fetch(
          `https://yuque.antfin-inc.com/api/comments/floor?commentable_type=Doc&commentable_id=${item.id}&include_section=true&include_to_user=true&include_reactions=true`,
          {
            headers: {
              accept: "application/json",
              "accept-language": "zh-CN,zh;q=0.9",
              "content-type": "application/json",
              "sec-ch-ua":
                '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"',
              "sec-ch-ua-mobile": "?0",
              "sec-ch-ua-platform": '"macOS"',
              "sec-fetch-dest": "empty",
              "sec-fetch-mode": "cors",
              "sec-fetch-site": "same-origin",
              "x-csrf-token": "7g3LVrMMDcljwFdl3GBLLIRy",
              "x-requested-with": "XMLHttpRequest",
            },
            referrerPolicy: "strict-origin-when-cross-origin",
            body: null,
            method: "GET",
            mode: "cors",
            credentials: "include",
          }
        )
          .then((res) => res.json())
          .then((res) => {
            return {
              ...item,
              comments: res.data.comments,
            };
          });
      })
    );
  }

  function openActivityModal(list) {
    layui.layer.open(
      {
        type: 1, // page 层类型
        area: ["800px", "500px"],
        title: "活动报名情况",
        shade: 0.6, // 遮罩透明度
        shadeClose: true, // 点击遮罩区域,关闭弹层
        maxmin: true, // 允许全屏最小化
        anim: 0, // 0-6 的动画形式,-1 不开启
        content: `
        <div style="padding: 24px">
          ${list.map((item) => {
            let manCount = 0;
            let womanCount = 0;
            let unknownCount = 0;

            item.comments.forEach((comment) => {
              const content = getInnerText(comment.body);
              comment.checked = true;
              const MAN_REG = /(\d+)\s?男/;
              const WOMAN_REG = /(\d+)\s?女/;
              const UNKNOWN_REG = /\+(\d+)/;
              if (MAN_REG.test(content)) {
                manCount += +MAN_REG.exec(content)[1];
                return;
              }
              if (WOMAN_REG.test(content)) {
                womanCount += +WOMAN_REG.exec(content)[1];
                return;
              }
              if (UNKNOWN_REG.test(content)) {
                unknownCount += +UNKNOWN_REG.exec(content)[1];
                return;
              }
              comment.checked = false;
            });

            const listHTML = item.comments
              .map((comment) => {
                const content = getInnerText(comment.body);
                return `<a class="${USER_ITEM_CLASS_NAME} ${
                  !comment.checked ? "unchecked" : ""
                }" href="https://yuque.antfin-inc.com/${
                  comment.user.login
                }" target="_blank">
                <img src="${comment.user.avatar_url}"/>
                <div>
                  <div>${comment.user.name}</div>
                  <div style="font-size: 12px; color: gray; margin-top: 4px;">${content}</div>
                </div>
              </a>`;
              })
              .join("");

            const status = (() => {
              const personCount = manCount + womanCount + unknownCount;
              if (item.manCount && item.womanCount && !item.reversable) {
                if (
                  manCount >= item.manCount &&
                  womanCount >= item.womanCount
                ) {
                  return `<span class="layui-badge layui-bg-green">已满人</span>`;
                }
                if (personCount >= item.manCount + item.womanCount) {
                  return `<span class="layui-badge layui-bg-orange">满人,但男女未满</span>`;
                }
                return `<span class="layui-badge layui-bg-red">未满人</span>`;
              }
              if (item.personCount) {
                if (personCount >= item.personCount) {
                  return `<span class="layui-badge layui-bg-green">已满人</span>`;
                }
                return `<span class="layui-badge layui-bg-red">未满人</span>`;
              }
            })();

            return `
              <div class="layui-card">
                <div class="layui-card-header" style="display: flex; justify-content: space-between;">
                  <a href="${item.url}" target="_blank">🔗 ${item.title}</a>

                </div>
                <div class="layui-card-body">
                  <div class="${USER_LIST_CLASS_NAME}">
                    ${listHTML}
                  </div>
                  <div class="layui-card-footer">
                    <span>要求:${item.persons}</span>
                    <span>当前:${manCount}男${womanCount}女${
              unknownCount ? `${unknownCount}未知` : ""
            },共${manCount + womanCount}人</span>
                    ${status}
                  </div>
                </div>
              </div>
            `;
          })}
        </div>
      `,
      },
      2000
    );
  }

  initStyle();
  initBtn();

  layui.dropdown.render({
    elem: `.${BTN_CLASS_NAME}`,
    data: [
      {
        title: "复制本周活动信息 Markdown",
        id: "copy week markdown",
      },
      {
        title: "查看本周活动报名情况",
        id: "check sign up",
      },
    ],
    click: async function ({ id }) {
      let list = await getActivesInfo(
        dayjs().startOf("week"),
        dayjs().endOf("week")
      );
      if (id === "copy week markdown") {
        copyMarkdownInfo(list);
      }
      if (id === "check sign up") {
        list = await getCommentsList(list);
        openActivityModal(list);
      }
    },
  });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址