// ==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);
}
},
});
})();