v1.7.0: 新增私信评论区用户(wbi签名+永久按mid去重+2~4分钟间隔)、安装统计上报、有新版本提示
This commit is contained in:
1 parent
38b90c1448
commit
0cce6cc2bb
5 files changed
+344
-9
No files matched your search
+243
-1
@@ -136,6 +136,194 @@
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
// ============ 私信(web_im send_msg,带 wbi 签名) ============
|
||||
// B站私信接口:POST https://api.vc.bilibili.com/web_im/v1/web_im/send_msg
|
||||
// 鉴权 wbi(w_rid + wts)+ csrf;只支持 msg_type=1(文字)/2(图)/5(撤回)
|
||||
// 关键风控:21047「对方主动回复或关注你前,最多发送1条」→ 必须按 mid 永久去重
|
||||
const MIXIN_INDEX = [46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52];
|
||||
|
||||
/* MD5 (RFC 1321),私信 w_rid 需要;下面用标准实现,测试向量见下方 selftest */
|
||||
function md5(str) {
|
||||
function rl(n, c) { return (n << c) | (n >>> (32 - c)); }
|
||||
function au(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF); var m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); }
|
||||
function cmn(q, a, b, x, s, t) { return au(rl(au(au(a, q), au(x, t)), s), b); }
|
||||
function ff(a, b, c, d, x, s, t) { return cmn((b & c) | (~b & d), a, b, x, s, t); }
|
||||
function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & ~d), a, b, x, s, t); }
|
||||
function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); }
|
||||
function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | ~d), a, b, x, s, t); }
|
||||
function utf8Bytes(s) {
|
||||
var out = [], i, c;
|
||||
for (i = 0; i < s.length; i++) {
|
||||
c = s.charCodeAt(i);
|
||||
if (c < 0x80) out.push(c);
|
||||
else if (c < 0x800) { out.push(0xC0 | (c >> 6), 0x80 | (c & 0x3F)); }
|
||||
else if (c >= 0xD800 && c <= 0xDBFF && i + 1 < s.length) {
|
||||
var c2 = s.charCodeAt(++i);
|
||||
var cp = 0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00);
|
||||
out.push(0xF0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3F), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F));
|
||||
} else { out.push(0xE0 | (c >> 12), 0x80 | ((c >> 6) & 0x3F), 0x80 | (c & 0x3F)); }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function toBlocks(s) {
|
||||
var b = utf8Bytes(s), n = b.length;
|
||||
var nb = ((n + 8) >> 6) + 1, blks = new Array(nb * 16).fill(0);
|
||||
for (var i = 0; i < n; i++) blks[i >> 2] |= b[i] << ((i % 4) * 8);
|
||||
blks[i >> 2] |= 0x80 << ((i % 4) * 8);
|
||||
blks[nb * 16 - 2] = n * 8;
|
||||
return blks;
|
||||
}
|
||||
var x = toBlocks(str), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878;
|
||||
for (var i = 0; i < x.length; i += 16) {
|
||||
var oa = a, ob = b, oc = c, od = d;
|
||||
a = ff(a, b, c, d, x[i], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819); b = ff(b, c, d, a, x[i + 3], 22, -1044525330);
|
||||
a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426); c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983);
|
||||
a = ff(a, b, c, d, x[i + 8], 7, 1770035416); d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
|
||||
a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290); b = ff(b, c, d, a, x[i + 15], 22, 1236535329);
|
||||
a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632); c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i], 20, -373897302);
|
||||
a = gg(a, b, c, d, x[i + 5], 5, -701558691); d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848);
|
||||
a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961); b = gg(b, c, d, a, x[i + 8], 20, 1163531501);
|
||||
a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784); c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734);
|
||||
a = hh(a, b, c, d, x[i + 5], 4, -378558); d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556);
|
||||
a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632); b = hh(b, c, d, a, x[i + 10], 23, -1094730640);
|
||||
a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i], 11, -358537222); c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189);
|
||||
a = hh(a, b, c, d, x[i + 9], 4, -640364487); d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651);
|
||||
a = ii(a, b, c, d, x[i], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905); b = ii(b, c, d, a, x[i + 5], 21, -57434055);
|
||||
a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606); c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799);
|
||||
a = ii(a, b, c, d, x[i + 8], 6, 1873313359); d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
|
||||
a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259); b = ii(b, c, d, a, x[i + 9], 21, -343485551);
|
||||
a = au(a, oa); b = au(b, ob); c = au(c, oc); d = au(d, od);
|
||||
}
|
||||
function hx(n) { var s = "", k; for (k = 0; k < 4; k++) s += "0123456789abcdef".charAt((n >> (k * 8 + 4)) & 0x0F) + "0123456789abcdef".charAt((n >> (k * 8)) & 0x0F); return s; }
|
||||
return hx(a) + hx(b) + hx(c) + hx(d);
|
||||
}
|
||||
|
||||
let WBI_CACHE = null;
|
||||
async function getWbiMixin() {
|
||||
if (WBI_CACHE) return WBI_CACHE;
|
||||
const data = await apiJSON("https://api.bilibili.com/x/web-interface/nav");
|
||||
const wi = data && data.data && data.data.wbi_img;
|
||||
if (!wi || !wi.img_url || !wi.sub_url) throw new Error("拿不到 wbi 密钥(未登录或接口异常)");
|
||||
const k1 = String(wi.img_url).split("/").pop().split(".")[0];
|
||||
const k2 = String(wi.sub_url).split("/").pop().split(".")[0];
|
||||
const raw = k1 + k2;
|
||||
let mixin = "";
|
||||
for (const i of MIXIN_INDEX) mixin += raw[i] || "";
|
||||
WBI_CACHE = mixin.slice(0, 32);
|
||||
return WBI_CACHE;
|
||||
}
|
||||
|
||||
async function signParams(params) {
|
||||
const mixin = await getWbiMixin();
|
||||
const p = Object.assign({}, params);
|
||||
p.wts = String(Math.floor(Date.now() / 1000));
|
||||
const clean = {};
|
||||
for (const k of Object.keys(p).sort()) clean[k] = String(p[k]).replace(/[!'()*]/g, "");
|
||||
const q = Object.keys(clean).map(k => k + "=" + encodeURIComponent(clean[k])).join("&");
|
||||
const w_rid = md5(q + mixin);
|
||||
return { q: q + "&w_rid=" + w_rid, w_rid: w_rid };
|
||||
}
|
||||
|
||||
async function getMyMid() {
|
||||
const data = await apiJSON("https://api.bilibili.com/x/web-interface/nav");
|
||||
const mid = data && data.data && data.data.mid;
|
||||
return Number(mid) || 0;
|
||||
}
|
||||
|
||||
// 已发过私信的 mid(永久去重:B站 21047 限制同一陌生人只能发 1 条)
|
||||
async function dmSentSet() {
|
||||
try {
|
||||
const c = await chrome.storage.local.get("dmSentMids");
|
||||
const arr = c.dmSentMids;
|
||||
return new Set(Array.isArray(arr) ? arr : []);
|
||||
} catch (e) { return new Set(); }
|
||||
}
|
||||
async function dmMarkSent(mid) {
|
||||
try {
|
||||
const c = await chrome.storage.local.get("dmSentMids");
|
||||
const arr = Array.isArray(c.dmSentMids) ? c.dmSentMids : [];
|
||||
const s = String(mid);
|
||||
if (!arr.includes(s)) { arr.push(s); if (arr.length > 5000) arr.splice(0, arr.length - 5000); }
|
||||
await chrome.storage.local.set({ dmSentMids: arr });
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
|
||||
// 取视频评论区用户 mid(去重、排除自己、排除已发过)
|
||||
async function getDmTargets(aid, limit, sentSet, myMid) {
|
||||
let mids = [];
|
||||
try {
|
||||
const data = await apiJSON("https://api.bilibili.com/x/v2/reply/main?oid=" + aid + "&type=1&ps=20&mode=3");
|
||||
const replies = (data && data.data && (data.data.replies || [])) || [];
|
||||
const hot = (data && data.data && (data.data.top_replies || [])) || [];
|
||||
const all = hot.concat(replies);
|
||||
for (const r of all) {
|
||||
const mid = Number(r && r.member && r.member.mid);
|
||||
if (!mid || mid === myMid) continue;
|
||||
const name = String((r.member && r.member.uname) || "");
|
||||
if (sentSet.has(String(mid))) continue;
|
||||
if (!mids.some(x => x.mid === mid)) mids.push({ mid: mid, name: name });
|
||||
if (mids.length >= limit) break;
|
||||
}
|
||||
} catch (e) { /* 接口不可用则返回空 */ }
|
||||
return mids;
|
||||
}
|
||||
|
||||
const DM_CODE_HINT = {
|
||||
"-101": "账号未登录或登录已过期",
|
||||
"-400": "请求错误(参数或签名问题)",
|
||||
"21015": "需要绑定手机号才能发私信",
|
||||
"21020": "发私信频率过快,已停止",
|
||||
"21026": "不能给自己发私信",
|
||||
"21046": "发私信频率过高,24小时后再发(已停止)",
|
||||
"21047": "对方未回复/未关注你前,最多只能发 1 条(该用户已跳过)",
|
||||
"25003": "对方隐私设置,无法发送",
|
||||
"25005": "你已拉黑对方"
|
||||
};
|
||||
|
||||
async function sendDm(mid, text) {
|
||||
const csrf = getCookie("bili_jct");
|
||||
if (!csrf) return { code: -999, message: "未读取到 bili_jct(登录态)" };
|
||||
const myMid = await getMyMid();
|
||||
if (!myMid) return { code: -998, message: "拿不到自己的 mid(未登录)" };
|
||||
const ts = Math.floor(Date.now() / 1000);
|
||||
const devId = (typeof crypto !== "undefined" && crypto.randomUUID)
|
||||
? crypto.randomUUID()
|
||||
: "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0, v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16);
|
||||
});
|
||||
const params = {
|
||||
"msg[sender_uid]": String(myMid),
|
||||
"msg[receiver_id]": String(mid),
|
||||
"msg[receiver_type]": "1",
|
||||
"msg[msg_type]": "1",
|
||||
"msg[msg_status]": "0",
|
||||
"msg[dev_id]": devId,
|
||||
"msg[timestamp]": String(ts),
|
||||
"msg[new_face_version]": "1",
|
||||
"msg[content]": JSON.stringify({ content: text }),
|
||||
"csrf_token": csrf,
|
||||
"csrf": csrf,
|
||||
"mobi_app": "web",
|
||||
"build": "0"
|
||||
};
|
||||
const signed = await signParams(params);
|
||||
const body = new URLSearchParams(signed.q.split("&").reduce((acc, kv) => {
|
||||
const i = kv.indexOf("="); acc[kv.slice(0, i)] = decodeURIComponent(kv.slice(i + 1)); return acc;
|
||||
}, {})).toString();
|
||||
const data = await apiJSON("https://api.vc.bilibili.com/web_im/v1/web_im/send_msg?" + signed.q, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Referer": "https://message.bilibili.com/",
|
||||
"Origin": "https://message.bilibili.com"
|
||||
},
|
||||
body: body
|
||||
});
|
||||
const code = data && data.code;
|
||||
return { code: code, message: DM_CODE_HINT[String(code)] || (data && data.message) || "" };
|
||||
}
|
||||
|
||||
|
||||
// ============ 关键词搜索 ============
|
||||
|
||||
async function searchVideos(keyword, order, page, tid) {
|
||||
@@ -686,6 +874,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 私信执行步:对当前视频的评论区用户发送(独立频率、永久按 mid 去重) ============
|
||||
// B站风控:21047 同一陌生人未回复/未关注前最多只能发 1 条;21046 频繁发会被限 24 小时
|
||||
// 所以默认间隔 2~4 分钟、每视频最多 3 人、同一 mid 永不重复
|
||||
const DM_GAP_MIN = 2 * 60 * 1000, DM_GAP_MAX = 4 * 60 * 1000;
|
||||
|
||||
async function runDmStep(task, video) {
|
||||
const cfg = task.config;
|
||||
const limit = Math.max(1, Math.min(5, Number(cfg.dmCount) || 1));
|
||||
try {
|
||||
step("准备私信:抓取评论区用户…");
|
||||
const myMid = await getMyMid();
|
||||
if (!myMid) { step("⚠️ 未登录,跳过私信", "fail"); return; }
|
||||
const sentSet = await dmSentSet();
|
||||
const targets = await getDmTargets(video.aid, limit, sentSet, myMid);
|
||||
if (!targets.length) { step("本视频无可私信对象(都已发过或无评论)"); return; }
|
||||
step("评论区可私信 " + targets.length + " 人");
|
||||
|
||||
for (let i = 0; i < targets.length; i++) {
|
||||
if (stopRequested()) { step("已手动停止,私信中断", "fail"); return; }
|
||||
const t = targets[i];
|
||||
const text = video.dmText || pickTemplate(video.title, cfg.keyword);
|
||||
const v = verifyMessage(text);
|
||||
if (!v.ok) { step("❌ 私信内容不合规:" + v.reason, "fail"); return; }
|
||||
step("私信 @" + t.name + "(" + (i + 1) + "/" + targets.length + "):" + text.slice(0, 24) + (text.length > 24 ? "…" : ""));
|
||||
const res = await sendDm(t.mid, text);
|
||||
if (res.code === 0) {
|
||||
task.dmSent = (task.dmSent || 0) + 1;
|
||||
await dmMarkSent(t.mid);
|
||||
relay({ type: "dmDone", mid: t.mid, name: t.name });
|
||||
step("✅ 私信已发送:@" + t.name, "ok");
|
||||
} else {
|
||||
step("❌ 私信失败 @" + t.name + ":" + res.message, "fail");
|
||||
// 频率类致命错误 → 直接停,避免把号搞废
|
||||
if (["21020", "21046", "-101", "-999"].indexOf(String(res.code)) >= 0) {
|
||||
step("触发私信风控,已停止后续私信", "fail");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (i < targets.length - 1) {
|
||||
const g = DM_GAP_MIN + Math.random() * (DM_GAP_MAX - DM_GAP_MIN);
|
||||
step("等待 " + Math.round(g / 1000) + " 秒后发下一条私信…");
|
||||
await interruptibleWait(g);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
step("私信出错:" + (e && e.message || e), "fail");
|
||||
}
|
||||
}
|
||||
|
||||
// 视频页内处理一条 → 等待 → 跳下一个(导航后由新页面的脚本续跑)
|
||||
async function processVideo(task) {
|
||||
running = true;
|
||||
@@ -713,6 +950,11 @@
|
||||
await recordOne(task, video, text, false);
|
||||
}
|
||||
|
||||
// 私信(可选,评论后对本视频评论区用户发送;风控更严,间隔更长)
|
||||
if (cfg.dmEnabled && !stopRequested()) {
|
||||
await runDmStep(task, video);
|
||||
}
|
||||
|
||||
task.index += 1;
|
||||
|
||||
if (task.index < task.queue.length && !stopRequested()) {
|
||||
@@ -726,7 +968,7 @@
|
||||
location.href = next.url;
|
||||
return; // 本页生命周期结束
|
||||
}
|
||||
finish(task, true, "执行完成:成功 " + (task.sent || 0) + " / 失败 " + (task.failed || 0));
|
||||
finish(task, true, "执行完成:评论成功 " + (task.sent || 0) + " / 失败 " + (task.failed || 0) + (task.dmSent ? ",私信 " + task.dmSent + " 条" : ""));
|
||||
} catch (e) {
|
||||
finish(task, false, "执行出错:" + (e && e.message || e));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user