Greasy Fork

Greasy Fork is available in English.

剧本杀活动通知生成器

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

当前为 2024-02-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         剧本杀活动通知生成器
// @namespace    https://github.com/heiyexing
// @version      2024-02-18
// @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';

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

  const BTN_ID = 'murder-mystery-btn';
  const USER_LIST_CLASS_NAME = 'murder-user-list';
  const USER_ITEM_CLASS_NAME = 'murder-user-item';

  let timeRange = [dayjs().startOf('week'), dayjs().endOf('week')];

  function initStyle() {
    const style = document.createElement('style');
    style.innerHTML = `
            #${BTN_ID} {
                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_ID} 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.id = BTN_ID;
    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,
    );
  }

  function openDatePickerModal([start, end]) {
    const modalIndex = layui.layer.open(
      {
        type: 1, // page 层类型
        title: '请选择日期范围',
        shade: 0.6, // 遮罩透明度
        area: ['655px', '400px'],
        shadeClose: true, // 点击遮罩区域,关闭弹层
        maxmin: true, // 允许全屏最小化
        anim: 0, // 0-6 的动画形式,-1 不开启
        content: `
            <div style="padding: 12px">
                <div id="date"></div>
            </div>
        `,
      },
      2000,
    );
    layui.laydate.render({
      elem: '#date',
      range: true,
      type: 'date',
      rangeLinked: true,
      weekStart: 1,
      show: true,
      theme: '#0271BD',
      position: 'static',
      value: `${start.format('YYYY-MM-DD')} - ${end.format('YYYY-MM-DD')}`,
      mark: {
        [dayjs().format('YYYY-MM-DD')]: '今天',
      },
      shortcuts: [
        {
          text: '本周',
          value: [
            new Date(+dayjs().startOf('week')),
            new Date(+dayjs().endOf('week')),
          ],
        },
        {
          text: '上周',
          value: [
            new Date(+dayjs().startOf('week').subtract(1, 'week')),
            new Date(+dayjs().endOf('week').subtract(1, 'week')),
          ],
        },
        {
          text: '下周',
          value: [
            new Date(+dayjs().startOf('week').add(1, 'week')),
            new Date(+dayjs().endOf('week').add(1, 'week')),
          ],
        },
        {
          text: '本月',
          value: [
            new Date(+dayjs().startOf('month')),
            new Date(+dayjs().endOf('month')),
          ],
        },
        // 更多选项 …
      ],
      done: function (value, startDate, endDate) {
        const [startStr, endStr] = value.split(' - ');
        timeRange = [
          dayjs(startStr, 'YYYY-MM-DD'),
          dayjs(endStr, 'YYYY-MM-DD'),
        ];
        layui.dropdown.reload(BTN_ID, {
          data: getDropdownItems(),
        });
        layui.layer.close(modalIndex);
      },
    });
  }

  initStyle();
  initBtn();

  function getDropdownItems() {
    return [
      {
        title: `日期范围:${timeRange[0].format('M-D')} - ${timeRange[1].format(
          'M-D',
        )}`,
        disabled: true,
      },
      {
        title: `更改日期范围`,
        id: 'edit date range',
      },
      {
        title: '复制活动信息 Markdown',
        id: 'copy week markdown',
      },
      {
        title: '查看活动报名情况',
        id: 'check sign up',
      },
    ];
  }

  layui.dropdown.render({
    elem: `#${BTN_ID}`,
    data: getDropdownItems(),
    click: async function ({ id }) {
      let list = await getActivesInfo(...timeRange);
      if (id === 'edit date range') {
        openDatePickerModal(timeRange);
      }
      if (id === 'copy week markdown') {
        copyMarkdownInfo(list);
      }
      if (id === 'check sign up') {
        list = await getCommentsList(list);
        openActivityModal(list);
      }
    },
  });
})();