码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
push.js6.2 KB
/**
 * Webhook 推送插件路由
 * 前端管理接口(PushConfig 组件调用)+ 外部 Webhook 接收接口。
 * 实际数据操作由 plugins/push/index.js 导出的方法完成。
 */
const express = require('express');
const router = express.Router();
const db = require('../lib/db');
const Auth = require('../lib/auth');
const Bot = require('../lib/bot');
const push = require('../plugins/push');

/** 当前用户 + 校验 bot 归属 */
async function owns(req, res) {
  const u = await Auth.currentUser(req);
  if (!u) { res.status(401).json({ ok: false, msg: '未登录' }); return null; }
  const botId = parseInt(req.body.bot_id || req.query.bot_id || 0, 10);
  if (!botId) { res.status(400).json({ ok: false, msg: '缺少 bot_id' }); return null; }
  const bot = await Bot.owned(botId, u.id);
  if (!bot) { res.status(403).json({ ok: false, msg: '机器人不存在或无权访问' }); return null; }
  return { user: u, bot };
}

// 列表:通道 + 定时推送 + 默认会话
router.post('/channels', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const data = await push.listForBot(ctx.bot.id);
    res.json({ ok: true, ...data });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 绑定 / 重新绑定
router.post('/bind', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const peerId = (req.body.default_peer || '').toString().trim();
    const ch = req.body.rebind
      ? await push.rebindChannel(ctx.bot.id, peerId)
      : await push.bindChannel(ctx.bot.id, peerId, '');
    res.json({ ok: true, channel: { ...ch, url: push.webhookUrl(ch.token) } });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 解绑(删除通道)
router.post('/unbind', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const id = parseInt(req.body.id, 10);
    if (!id) throw new Error('缺少 id');
    await push.deleteChannel(id, ctx.bot.id);
    res.json({ ok: true });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 更新通道(AI 开关 / AI 指令)
router.post('/channel_update', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const id = parseInt(req.body.id, 10);
    if (!id) throw new Error('缺少 id');
    const patch = {};
    if (req.body.ai_enabled !== undefined) patch.ai_enabled = req.body.ai_enabled ? 1 : 0;
    if (req.body.ai_prompt !== undefined) patch.ai_prompt = String(req.body.ai_prompt || '');
    await push.updateChannel(id, ctx.bot.id, patch);
    res.json({ ok: true });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 添加定时推送
router.post('/schedule_add', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const { content, time_desc, peer_id } = req.body;
    await push.addSchedule(ctx.bot.id, (peer_id || '').toString().trim(), {
      content,
      ai_enabled: !!req.body.ai_enabled,
      ai_prompt: '',
      time_desc,
    });
    res.json({ ok: true });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 取消定时推送
router.post('/schedule_del', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const id = parseInt(req.body.id, 10);
    if (!id) throw new Error('缺少 id');
    await push.deleteSchedule(id, ctx.bot.id);
    res.json({ ok: true });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// ==================== 勿扰 / 推送设置(DND)====================
const dnd = require('../lib/dnd');

// 列出该 bot 近期会话(供选择要设置勿扰的用户)+ 已有勿扰配置
router.post('/dnd_list', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    await dnd.ensure();
    // 近期有往来的 peer(最多 100 个)
    const peers = await db.rows(
      `SELECT peer_id, MAX(created_at) AS last_at, COUNT(*) AS cnt
       FROM messages WHERE bot_id=? AND peer_id IS NOT NULL AND peer_id<>''
       GROUP BY peer_id ORDER BY last_at DESC LIMIT 100`,
      [ctx.bot.id]
    );
    const dndRows = await db.rows('SELECT * FROM user_dnd WHERE bot_id=?', [ctx.bot.id]);
    const dndMap = {};
    dndRows.forEach(r => { dndMap[r.peer_id] = { enabled: r.enabled === 1, start_min: r.start_min, end_min: r.end_min, mask: r.mask }; });
    res.json({ ok: true, peers, dnd: dndMap, cats: dnd.CAT, defaultMask: dnd.DEFAULT_MASK });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 获取某个 peer 的勿扰配置
router.post('/dnd_get', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const peerId = (req.body.peer_id || '').toString().trim();
    if (!peerId) throw new Error('缺少 peer_id');
    const cfg = await dnd.getDnd(ctx.bot.id, peerId);
    res.json({ ok: true, dnd: cfg, cats: dnd.CAT, defaultMask: dnd.DEFAULT_MASK });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 保存某个 peer 的勿扰配置
router.post('/dnd_save', async (req, res) => {
  const ctx = await owns(req, res); if (!ctx) return;
  try {
    const peerId = (req.body.peer_id || '').toString().trim();
    if (!peerId) throw new Error('缺少 peer_id');
    await dnd.setDnd(ctx.bot.id, peerId, {
      enabled: !!req.body.enabled,
      start_min: parseInt(req.body.start_min, 10) || 0,
      end_min: parseInt(req.body.end_min, 10) || 0,
      mask: req.body.mask == null ? dnd.DEFAULT_MASK : (parseInt(req.body.mask, 10) | 0),
    });
    res.json({ ok: true });
  } catch (e) {
    res.json({ ok: false, msg: e.message });
  }
});

// 外部 Webhook 接收(无需登录):POST /api/push/webhook/<token>
router.post('/webhook/:token', async (req, res) => {
  try {
    const token = req.params.token;
    const body = req.body && Object.keys(req.body).length ? req.body : req.query;
    const result = await push.handleWebhook(token, body);
    res.json({ ok: true, ...result });
  } catch (e) {
    const status = e.status || 500;
    res.status(status).json({ ok: false, msg: e.message });
  }
});

module.exports = router;