qq-bot/index.ts

179 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from 'express';
import type { Request, Response, NextFunction } from 'express';
import fs from 'fs';
import path from 'path';
import cors from "cors";
import multer from "multer";
import * as douyin from './douyin'
const app = express();
const PORT = process.env.PORT || 6100;
const __dirname = path.dirname(new URL(import.meta.url).pathname);
app.use(cors());
// 仅解析明确类型;不要用 '*/*'
app.use(express.json({ limit: '10mb', type: ['application/json'] }));
app.use(express.urlencoded({ extended: true, limit: '10mb', type: ['application/x-www-form-urlencoded'] }));
// QQ机器人回显功能
app.post('/', async (req: Request, res: Response) => {
// 检查是否是消息类型的请求
if (!req.body || req.body.post_type != 'message') return
const { target_id, raw_message, message_type, user_id } = req.body;
console.log(`\n[QQ机器人] 收到${message_type}消息`);
console.log(`发送者ID: ${user_id || target_id}`);
console.log(`消息内容: ${raw_message}`);
// Match Douyin URL
// Like: https://v.douyin.com/YqgJL_phY_k/
const douyinUrlPattern = /https?:\/\/v\.douyin\.com\/[a-zA-Z0-9_-]+/;
const douyinMatch = raw_message.match(douyinUrlPattern);
try {
// 如果检测到抖音链接调用解析API
if (douyinMatch) {
const douyinUrl = douyinMatch[0];
console.log(`[抖音链接检测] 发现抖音链接: ${douyinUrl}`);
sendMsg(`[抖音链接检测] 发现抖音链接: ${douyinUrl},启动 Chromium 中...`, target_id);
douyin.downloadDouyinMedia(douyinUrl, target_id);
}
} catch (error) {
console.error(`[错误] 处理消息时发生错误:`, error);
sendMsg(String(error), target_id);
}
});
function sanitizeDirname(input: string) {
// 基础清洗:去除路径分隔符、控制字符,并截断长度(防止奇怪标题)
const s = (input || "default")
.trim()
.replace(/[\\/]+/g, "_")
.replace(/[<>:"|?*\u0000-\u001F]/g, "_")
.slice(0, 128) || "default";
return s;
}
const storage = multer.diskStorage({
destination: (req, _file, cb) => {
const title = sanitizeDirname(req.query.title as string);
const baseDir = process.env.DOWNLOAD_DIR || path.join(__dirname, "downloads");
const dest = req.query.type == 'image' ? path.join(baseDir, title) : baseDir;
fs.mkdir(dest, { recursive: true }, (err) => cb(err, dest));
}, filename: (req, file, cb) => {
const ext = path.extname(file.originalname || "");
const title = sanitizeDirname(req.query.title as string);
cb(null, `${title}_${Date.now()}_${Math.random().toString(36).slice(2)}${ext}`);
},
});
const upload = multer({
storage,
// 限制:单文件最大 1GB、最多 50 个字段(可按需调整)
limits: { fileSize: 1024 * 1024 * 1024, fields: 50, files: 50 },
fileFilter: (_req, file, cb) => {
// 只接受字段名以 file_ 开头的文件,其他拒绝
if (/^file_\d+$/i.test(file.fieldname)) cb(null, true);
else cb(new Error(`Unexpected file field: ${file.fieldname}`));
}
});
app.post("/upload", upload.any(), (req, res) => {
// 1) 取出文件multer.any() 把所有文件放在 req.files
const files = (req.files as Express.Multer.File[]) || [];
// 只保留我们关心的 file_*
const accepted = files.filter(f => /^file_\d+$/i.test(f.fieldname));
let meta = {
title: req.body.title,
type: req.query.type as 'video' | 'image',
target_id: req.query.target_id
};
console.log(meta);
console.log(`收到上传: ${accepted.length} 个文件`, files.map(f => f.path));
if (meta.target_id) {
const totalSize = accepted.reduce((sum, f) => sum + f.size, 0);
sendMsg(`[抖音下载] ${meta.title},已下载 ${accepted.length} 个文件,类型 ${meta.type},共 ${(totalSize/1024/1024).toFixed(2)} MB上传中...`, meta.target_id as string);
accepted.forEach(f => {
sendMediaMsg(f.path, meta.target_id as string, meta.type);
})
}
res.json({ ok: true, files: accepted.length, meta });
});
function sendMsg(msg: string, target_id: string) {
const replyMessage = {
user_id: String(target_id),
message: [
{
type: "text",
data: {
text: msg
}
}
]
}
const replyUrl = `http://localhost:30000/send_private_msg`;
console.log(`[发送消息] ${msg} -> ${target_id}`);
return fetch(replyUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(replyMessage)
});
}
function sendMediaMsg(filePath: string, target_id: string, type: 'video' | 'image') {
const mediaMessage = {
user_id: String(target_id),
message: [
{
type: type,
data: {
file: `file://${filePath}`
}
}
]
}
const replyUrl = `http://localhost:30000/send_private_msg`;
console.log(`[发送媒体消息] ${type} - ${filePath} -> ${target_id}`);
return fetch(replyUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(mediaMessage)
});
}
app.use((error: any, req: Request, res: Response, next: NextFunction) => {
const timestamp = new Date().toISOString();
console.error(`\n[${timestamp}] 错误:`, error);
res.status(500).json({
success: false,
message: '服务器内部错误',
error: error.message
});
});
// 启动服务器
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
console.log(`With env:`, { DOWNLOAD_DIR: process.env.DOWNLOAD_DIR, PORT: process.env.PORT });
});