码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
ng-webot/lib/dnd.js
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,
};