码桶
发现社区成员的开源项目
dnd.js4.3 KB
/**
* 勿扰 / 推送设置(DND)
* --------------------------------------------------
* 每个用户(微信会话 peer)可独立设置「勿扰时段」:在该时段内,
* 机器人主动发出的某些类别消息会被延后/静音。
*
* 类别(可让用户自行选择哪些在勿扰时段被静音):
* webhook 外部 Webhook 推送(推送插件 外部系统)
* schedule 定时推送(推送插件 非 AI 的定时内容)
* reminder 提醒(定时提醒插件)
* ai AI 主动发的消息(如 AI 润色的定时推送;默认不禁,符合“AI 主动发的仍要发”)
*
* 默认静音类别:webhook + schedule + reminder(不含 ai)。
*
* 存储:user_dnd(bot_id, peer_id, enabled, start_min, end_min, mask)
* start_min / end_min 为「当日分钟数」(0~1439),支持跨午夜(start > end 表示跨天)。
*/
const db = require('./db');
const CAT = { webhook: 1, schedule: 2, reminder: 4, ai: 8 };
// 默认:静音 推送/定时/提醒,但不静音 AI 主动发的
const DEFAULT_MASK = CAT.webhook | CAT.schedule | CAT.reminder; // 7
let ready = false;
async function ensure() {
if (ready) return;
await db.exec(`CREATE TABLE IF NOT EXISTS user_dnd (
bot_id INTEGER NOT NULL,
peer_id VARCHAR(128) NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,
start_min INTEGER NOT NULL DEFAULT 1320,
end_min INTEGER NOT NULL DEFAULT 480,
mask INTEGER NOT NULL DEFAULT 7,
PRIMARY KEY (bot_id, peer_id)
)`);
ready = true;
}
function normMin(m) {
m = parseInt(m, 10);
if (isNaN(m)) return 0;
if (m < 0) m = 0;
if (m > 1439) m = 1439;
return m;
}
function nowMin(d = new Date()) {
return d.getHours() * 60 + d.getMinutes();
}
/** 当前是否处于该 peer 的勿扰窗口内(含跨午夜) */
function inWindow(cfg, d = new Date()) {
if (!cfg || !cfg.enabled) return false;
const nm = nowMin(d);
const { start_min, end_min } = cfg;
if (start_min === end_min) return false; // 起止相同视为未设置
if (start_min < end_min) return nm >= start_min && nm < end_min;
return nm >= start_min || nm < end_min; // 跨午夜
}
/** 计算当前勿扰窗口的结束时间点(用于延后消息) */
function dndEndDate(cfg, d = new Date()) {
const nm = nowMin(d);
const end = new Date(d);
end.setHours(Math.floor(cfg.end_min / 60), cfg.end_min % 60, 0, 0);
if (cfg.start_min < cfg.end_min) {
if (nm >= cfg.end_min) end.setDate(end.getDate() + 1);
} else {
if (nm >= cfg.start_min) end.setDate(end.getDate() + 1);
}
return end;
}
async function getDnd(botId, peerId) {
await ensure();
const r = await db.row('SELECT * FROM user_dnd WHERE bot_id=? AND peer_id=?', [botId, peerId]);
if (!r) return { enabled: false, start_min: 1320, end_min: 480, mask: DEFAULT_MASK, exists: false };
return { enabled: r.enabled === 1, start_min: r.start_min, end_min: r.end_min, mask: r.mask, exists: true };
}
async function setDnd(botId, peerId, cfg) {
await ensure();
const enabled = cfg.enabled ? 1 : 0;
const start_min = normMin(cfg.start_min);
const end_min = normMin(cfg.end_min);
const mask = cfg.mask == null ? DEFAULT_MASK : (cfg.mask | 0);
try {
await db.exec(
'INSERT INTO user_dnd (bot_id, peer_id, enabled, start_min, end_min, mask) VALUES (?,?,?,?,?,?)',
[botId, peerId, enabled, start_min, end_min, mask]
);
} catch (e) {
await db.exec(
'UPDATE user_dnd SET enabled=?, start_min=?, end_min=?, mask=? WHERE bot_id=? AND peer_id=?',
[enabled, start_min, end_min, mask, botId, peerId]
);
}
}
async function isDndActive(botId, peerId, d = new Date()) {
const cfg = await getDnd(botId, peerId);
return inWindow(cfg, d);
}
/** 该类别的主动消息在当前是否应被勿扰拦截 */
async function shouldBlock(botId, peerId, cat, d = new Date()) {
const cfg = await getDnd(botId, peerId);
if (!cfg.enabled) return false;
if (!inWindow(cfg, d)) return false;
return (cfg.mask & (CAT[cat] || 0)) !== 0;
}
/** 工具:格式化分钟数为 HH:MM */
function fmtMin(m) {
m = normMin(m);
return Math.floor(m / 60) + ':' + String(m % 60).padStart(2, '0');
}
module.exports = {
CAT, DEFAULT_MASK, ensure, getDnd, setDnd, isDndActive, shouldBlock, dndEndDate, inWindow, nowMin, fmtMin,
};