MCBBS抓取新帖mod

获取版块最新帖子。

// ==UserScript==
// @name        MCBBS抓取新帖mod
// @namespace   io.github.msbbc
// @match       *://www.mcbbs.net/*
// @icon         https://beta.mcbbs.net/favicon.ico
// @version     0.7.4.7
// @author      axototl
// @license     AGPL-3.0-or-later
// @grant       GM_registerMenuCommand
// @grant       GM_unregisterMenuCommand
// @grant       GM_addValueChangeListener
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_notification
// @grant       GM_info
// @grant       GM_openInTab
// @require     https://update.greasyfork.org/scripts/476522/1285584/Config_Manager.js
// @require     https://update.greasyfork.org/scripts/478724/1284835/Notify.js
// @description 获取版块最新帖子。
// ==/UserScript==
'use strict';

const vio = (() => {
    if (GM_info.scriptHandler === "Violentmonkey") return true;
    GM_addElement("p", {
        textContent: `脚本 ${GM_info.script.name} 优先适配暴力猴, 建议使用Violentmonkey(暴力猴)获得更好体验.`,
        style: "position: fixed; bottom: 0; right: 0; color: #F92672; font-size: 16px; font-weight: bold;"
    }).onclick = () => GM_openInTab("https://violentmonkey.github.io/get-it/");
    return false;
})();

// JS Parser
const parse_result = (() => {
    const patt = /^var\s+newthread.*?$/m; // 用于匹配JS代码开头
    const numpatt = />([\d\-]+)<\/a>/; // 用于匹配回复数
    const titler = />([^<]+)<\/a>$/; // 用于匹配标题
    const ss = /href="([^>]+)">([^<]+)<\/a><\/em>/; // 用于匹配最后回复的URL
    const lastposter_match = />([^<]*?)<\/a><\/cite>/; //最后回帖者
    const match_icn = /src="(.*?)"/; // 帖子的icon
    return js =>
        (() => {
            const matches = js.match(patt);
            if (matches && matches.length > 0) {
                js = matches[0];
                const r = // 缓解性能问题
                      (0, eval)(`let table,removetbodyrow=()=>0;${js};newthread`);
                if (config.debug) console.debug(r);
                return r;
            }
        })()?.map(item => {
            const item2 = {};
            item2.title = titler.exec(item.thread.common.val)[1];
            item2.reply_num = numpatt.exec(item.thread.num.val)[1] | 0;
            item2.uri = "https://www.mcbbs.net/";
            let tmp = ss.exec(item.thread.lastpost.val);
            item2.lastpost_time = tmp[2];
            item2.lastposter = lastposter_match.exec(item.thread.lastpost.val)[1];
            item2.tid = item.tid;
            if (config.jmp_to_lastpost) {
                const uri = tmp[1].replaceAll("&amp;", "&");
                item2.uri += uri;
            } else {
                item2.uri += "thread-" + item.tid + "-1-1.html";
            }
            tmp = match_icn.exec(item.thread.icn.val);
            if (tmp) item2.icon = tmp[1];
            else item2.icon = "https://www.mcbbs.net/favicon.ico";
            return item2;
        });
})();

[
    {
        name: "experimental",
        type: "bool",
        desc: "实验性功能",
        default: false,
        // 当前版本暂无实验性功能
    },
    {
        name: "run",
        desc: "程序运行",
        default: true,
        type: "bool",
        temp: true,
        judge: () => false,
    },
    {
        name: "debug",
        type: "bool",
        desc: "调试模式",
        default: false,
    },
    {
        name: "fids",
        type: "other",
        desc: "设置监听板块FID",
        tips: "请输入板块FID,用半角逗号 (,) 隔开",
        default: "110,266,431,1566"
    },
    {
        name: "silent",
        type: "bool",
        desc: "静音发送提醒",
        default: false,
    },
    {
        name: "backtime",
        type: "uint",
        desc: "设置轮询时间间隔",
        default: 5e3 | 0,
    },
    {
        name: "maxreply",
        type: "int",
        desc: "设置最多回复数 (设置非正数禁用)",
        default: -1,
    },
    {
        name: "renotify",
        type: "bool",
        desc: "同一个帖子重新提醒",
        default: true,
    },
    {
        name: "jmp_to_lastpost",
        type: "bool",
        desc: "跳转到最新回复",
        default: true,
    },
    {
        name: "root_page",
        type: "other",
        desc: "手动设置运行页面",
        tips: "请输入运行页面,以/开头,比如运行在坛规帖页面就是:/thread-7808-1-1.html",
        default: "/thread-7808-1-1.html",
        callback: ((_n, _ov, nv) => (location.pathname === nv || ov === location.pathname) ? location.reload() : 0),
        judge: (name, val) => !!(typeof val === "string"),
    },
    {
        name: "root_only",
        type: "bool",
        desc: "仅运行在指定页面",
        default: false,
        callback: location.reload,
    },
    {
        name: "banned",
        type: "other",
        desc: "设置不推送的用户",
        default: "",
        tips: "设置屏蔽的用户\n 可以设置成自己,这样就不会收到重复推送"
    },
    {
        name: "title_filter",
        type: "other",
        desc: "设置正则屏蔽标题",
        tips: "设置正则表达式,若命中表达式则不推送",
        default: null,
    },
].forEach(register);

if (config.root_only) GM_registerMenuCommand("打开运行页", () => GM_openInTab(config.root_page));

const _Prefix = "GNP-";
const storage = (config.root_only ? sessionStorage : localStorage);
const getItem = id => storage.getItem(_Prefix + id);
const setItem = (id, val) => storage.setItem(_Prefix + id, val);
const clear = () => {
    const arr = [];
    for (let i = 0; i < storage.length; ++i) {
        const k = storage.key(i);
        if (k.startsWith(_Prefix)) arr.push(k);
    }
    arr.forEach(k => storage.removeItem(k));
}

// core fn
async function getposts(fid) {
    if (!navigator.onLine) return null;
    let result = await (async () => {
        const uri = "https://www.mcbbs.net/forum.php?mod=ajax&action=forumchecknew&inajax=yes&inajax=1&ajaxtarget=forumnew&uncheck=2&" +
                    `fid=${fid}&time=${Math.round(new Date().getTime() / 1000) - Math.round(Math.random()*100)%50 - 25}`;
        let r;
        try {const s = await fetch(uri); r = await s.text();}
        catch(e) {console.error(e); return null;}
        return parse_result(r);
    })();
    if (!result) return;
    if (config.maxreply >= 0)
        result = result.filter(item => item.reply_num <= config.maxreply);
    const ban = config.banned;
    result = result.filter(item => {
        const rply = getItem(item.tid.toString()) ?? 0;
        const rn = item.reply_num;
        if (rply < rn) {
            setItem(item.tid, rn);
            return ban != item.lastposter;
        } return false;
    });
    return result;
}

function trans_params(paras) {
    const rt = new URLSearchParams();
    for (const x in paras) rt.append(x, paras[x]);
    return rt;
}


function pusher(item, fid) {
    if (item.title.match(config.title_filter)) return;
    const fresh = (item.reply_num == 0) | 0;
    const body = ["最后回复时间", "发布时间"][fresh]
    +':'+ item.lastpost_time +'\n'
    + ["回复者", "发布者"][fresh] +':'+ item.lastposter
    + "\n【来自" + fid + "号版块】";
    item.title = (fresh ? "【新帖】": "【有"+ item.reply_num + "条回复】") + item.title;
    notify({
        text: body,
        title: item.title,
        image: item.icon,
        tag: _Prefix + item.tid,
        url: item.uri,
        silent: config.silent,
    });
    // push(item.uri, subj, item.title, item.icon, item.tid);
}

// main function

let need_clean = false;
(async () => {
    if (config.root_only && location.pathname !== config.root_page) return;
    const delay = n => new Promise(r => setTimeout(r, n));
    for (;;) {
        if (!config.fids) return;
        const arr = config.fids.split(",");
        for (const fid of arr) {
            if (need_clean) clear(), need_clean = false;
            (await getposts(fid))?.forEach(it => pusher(it, fid));
            await delay(config.backtime);
        }
    }
})();

GM_registerMenuCommand("清除缓存(可能导致重复推送)", () => need_clean = true);