Compare commits

..

7 Commits

Author SHA1 Message Date
linyuchen
911841401a refactor: utils structure 2024-03-18 11:10:53 +08:00
linyuchen
4c6bd3df0b refactor: utils structure 2024-03-18 11:07:08 +08:00
linyuchen
c5932bcd98 refactor: utils update 2024-03-18 10:56:50 +08:00
linyuchen
3abc9f2ae0 chore: ver 3.16.1 2024-03-18 09:53:41 +08:00
linyuchen
e716c28e9a Merge branch 'dev'
# Conflicts:
#	src/main/main.ts
#	src/version.ts
2024-03-18 09:53:13 +08:00
linyuchen
9209ae766c feat: 接收戳一戳开关
feat: message_sent 事件添加 target_id 字段
feat: 回复临时消息
2024-03-18 09:49:47 +08:00
linyuchen
af8cf1882c refactor: member qq_level 2024-03-18 06:59:11 +08:00
43 changed files with 325 additions and 555 deletions

View File

@@ -1,10 +1,10 @@
{
"manifest_version": 4,
"type": "extension",
"name": "LLOneBot v3.16.0",
"name": "LLOneBot v3.16.1",
"slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新",
"version": "3.16.0",
"version": "3.16.1",
"icon": "./icon.jpg",
"authors": [
{

View File

@@ -1,6 +1,10 @@
import fs from "fs";
import {Config, OB11Config} from './types';
import {mergeNewProperties} from "./utils";
import {mergeNewProperties} from "./utils/helper";
import path from "node:path";
import {selfInfo} from "./data";
import {DATA_DIR} from "./utils";
export const HOOK_LOG = false;
@@ -44,6 +48,7 @@ export class ConfigUtil {
reportSelfMessage: false,
autoDeleteFile: false,
autoDeleteFileSecond: 60,
enablePoke: false
};
if (!fs.existsSync(this.configPath)) {
@@ -84,3 +89,8 @@ export class ConfigUtil {
}
}
}
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}

View File

@@ -6,8 +6,9 @@ import {
type SelfInfo
} from '../ntqqapi/types'
import {type FileCache, type LLOneBotError} from './types'
import {isNumeric, log} from "./utils";
import {NTQQGroupApi} from "../ntqqapi/api/group";
import {log} from "./utils/log";
import {isNumeric} from "./utils/helper";
export const selfInfo: SelfInfo = {
uid: '',

View File

@@ -1,9 +1,11 @@
import {Level} from "level";
import {type GroupNotify, RawMessage} from "../ntqqapi/types";
import {DATA_DIR, log} from "./utils";
import {DATA_DIR} from "./utils";
import {selfInfo} from "./data";
import {FileCache} from "./types";
import {log} from "./utils/log";
type ReceiveTempUinMap = Record<string, string>;
class DBUtil {
public readonly DB_KEY_PREFIX_MSG_ID = "msg_id_";
@@ -11,8 +13,9 @@ class DBUtil {
public readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_";
public readonly DB_KEY_PREFIX_FILE = "file_";
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = "group_notify_";
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = "received_temp_uin_map";
public db: Level;
public cache: Record<string, RawMessage | string | FileCache | GroupNotify> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number;
/*
@@ -67,6 +70,17 @@ class DBUtil {
}, expiredMilliSecond)
}
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try{
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP));
}catch (e) {
}
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap;
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data;
this.db.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then();
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId

View File

@@ -1,6 +1,7 @@
import express, {Express, json, Request, Response} from "express";
import {getConfigUtil, log} from "../utils";
import http from "http";
import {log} from "../utils/log";
import {getConfigUtil} from "../config";
type RegisterHandler = (res: Response, payload: any) => Promise<any>

View File

@@ -1,7 +1,8 @@
import {WebSocket, WebSocketServer} from "ws";
import {getConfigUtil, log} from "../utils";
import urlParse from "url";
import {IncomingMessage} from "node:http";
import {log} from "../utils/log";
import {getConfigUtil} from "../config";
class WebsocketClientBase {
private wsClient: WebSocket

View File

@@ -24,6 +24,7 @@ export interface Config {
autoDeleteFile?: boolean
autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径
enablePoke?: boolean
}
export interface LLOneBotError {

View File

@@ -1,130 +1,13 @@
import * as path from "node:path";
import { selfInfo } from "./data";
import { ConfigUtil } from "./config";
import fs from "fs";
import crypto from "crypto";
import ffmpeg from "fluent-ffmpeg";
import util from "util";
import { encode, getDuration, isWav } from "silk-wasm";
import fs from 'fs';
import * as crypto from 'crypto';
import { v4 as uuidv4 } from "uuid";
import ffmpeg from "fluent-ffmpeg"
import * as https from "node:https";
import { version } from "../version";
export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
// 判断是否为最新版本
export async function checkVersion() {
const latestVersionText = await getRemoteVersion();
const latestVersion = latestVersionText.split(".");
const currentVersion = version.split(".");
for (let k in [0, 1, 2]) {
if (latestVersion[k] > currentVersion[k]) {
return { result: false, version: latestVersionText };
}
}
return { result: true, version: version };
}
export async function updateLLOneBot() {
let mirrorGithubList = ["https://mirror.ghproxy.com"];
const latestVersion = await getRemoteVersion();
if (latestVersion && latestVersion != "") {
const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip";
const realUrl = mirrorGithubList[0] + downloadUrl;
}
return false;
}
export async function getRemoteVersion() {
let mirrorGithubList = ["https://521github.com"];
let Version = "";
for (let i = 0; i < mirrorGithubList.length; i++) {
let mirrorGithub = mirrorGithubList[i];
let tVersion = await getRemoteVersionByMirror(mirrorGithub);
if (tVersion && tVersion != "") {
Version = tVersion;
break;
}
}
return Version;
}
export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = "error";
let reqPromise = async function (): Promise<string> {
return new Promise((resolve, reject) => {
https.get(mirrorGithub + "/LLOneBot/LLOneBot/releases", res => {
let list = [];
res.on('data', chunk => {
list.push(chunk);
});
res.on('end', () => {
resolve(Buffer.concat(list).toString());
});
}).on('error', err => {
reject();
});
});
}
try {
releasePage = await reqPromise();
if (releasePage === "error") return "";
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0];
}
catch { }
return "";
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
let currentDateTime = new Date().toLocaleString();
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(DATA_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
})
}
import {encode, getDuration, isWav} from "silk-wasm";
import path from "node:path";
import {v4 as uuidv4} from "uuid";
import {DATA_DIR} from "./index";
import {log} from "./log";
import {getConfigUtil} from "../config";
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
@@ -134,11 +17,6 @@ export function isGIF(path: string) {
return buffer.toString() === 'GIF8'
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
@@ -184,25 +62,6 @@ export async function file2base64(path: string) {
return result;
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key];
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]);
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key];
}
}
});
}
export function checkFfmpeg(newPath: string = null): Promise<boolean> {
return new Promise((resolve, reject) => {
if (newPath) {
@@ -290,7 +149,8 @@ export async function encodeSilk(filePath: string) {
const pcm = fs.readFileSync(filePath);
const silk = await encode(pcm, 0);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => { });
fs.unlink(wavPath, (err) => {
});
log(`语音文件${filePath}转换成功!`, pttPath)
return {
converted: true,
@@ -324,7 +184,14 @@ export async function encodeSilk(filePath: string) {
export async function getVideoInfo(filePath: string) {
const size = fs.statSync(filePath).size;
return new Promise<{ width: number, height: number, time: number, format: string, size: number, filePath: string }>((resolve, reject) => {
return new Promise<{
width: number,
height: number,
time: number,
format: string,
size: number,
filePath: string
}>((resolve, reject) => {
ffmpeg(filePath).ffprobe((err, metadata) => {
if (err) {
reject(err);
@@ -347,7 +214,6 @@ export async function getVideoInfo(filePath: string) {
})
}
export async function encodeMp4(filePath: string) {
let videoInfo = await getVideoInfo(filePath);
log("视频信息", videoInfo)
@@ -372,11 +238,6 @@ export async function encodeMp4(filePath: string) {
return videoInfo
}
export function isNull(value: any) {
return value === undefined || value === null;
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
@@ -399,4 +260,4 @@ export function calculateFileMD5(filePath: string): Promise<string> {
reject(err);
});
});
}
}

View File

@@ -0,0 +1,46 @@
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key];
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]);
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key];
}
}
});
}
export function isNull(value: any) {
return value === undefined || value === null;
}

View File

@@ -1,342 +1,10 @@
import * as path from "node:path";
import {selfInfo} from "../data";
import {ConfigUtil} from "../config";
import util from "util";
import {encode, getDuration, isWav} from "silk-wasm";
import fs from 'fs';
import * as crypto from 'crypto';
import {v4 as uuidv4} from "uuid";
import ffmpeg from "fluent-ffmpeg"
export * from './file'
export * from './helper'
export * from './log'
export * from './qqlevel'
export * from './qqpkg'
export * from './update'
export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
let currentDateTime = new Date().toLocaleString();
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(DATA_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
})
}
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
const fd = fs.openSync(path, 'r');
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
return buffer.toString() === 'GIF8'
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
let result = {
err: "",
data: ""
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err) {
result.err = err.toString();
}
return result;
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key];
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]);
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key];
}
}
});
}
export function checkFfmpeg(newPath: string = null): Promise<boolean> {
return new Promise((resolve, reject) => {
if (newPath) {
ffmpeg.setFfmpegPath(newPath);
ffmpeg.getAvailableFormats((err, formats) => {
if (err) {
log('ffmpeg is not installed or not found in PATH:', err);
resolve(false)
} else {
log('ffmpeg is installed.');
resolve(true);
}
})
}
});
}
export async function encodeSilk(filePath: string) {
const fsp = require("fs").promises
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7;
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: "r",
});
const fileHeader = buffer.toString("hex", 0, bytesToRead);
return fileHeader;
} catch (err) {
console.error("读取文件错误:", err);
return;
}
}
async function isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath));
}
// async function getAudioSampleRate(filePath: string) {
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try {
const fileName = path.basename(filePath);
const pttPath = path.join(DATA_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath);
const wavPath = pttPath + ".wav"
if (!_isWav) {
log(`语音文件${filePath}正在转换成wav`)
// let voiceData = await fsp.readFile(filePath)
await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav").audioChannels(2).on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", () => {
filePath = wavPath
resolve(wavPath);
});
})
}
// const sampleRate = await getAudioSampleRate(filePath) || 0;
// log("音频采样率", sampleRate)
const pcm = fs.readFileSync(filePath);
const silk = await encode(pcm, 0);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => { });
log(`语音文件${filePath}转换成功!`, pttPath)
return {
converted: true,
path: pttPath,
duration: silk.duration,
};
} else {
const pcm = fs.readFileSync(filePath);
let duration = 0;
try {
duration = getDuration(pcm);
} catch (e) {
log("获取语音文件时长失败", filePath, e.stack)
duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log("使用文件大小估算时长", duration)
}
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}
export async function getVideoInfo(filePath: string) {
const size = fs.statSync(filePath).size;
return new Promise<{ width: number, height: number, time: number, format: string, size: number, filePath: string }>((resolve, reject) => {
ffmpeg(filePath).ffprobe( (err, metadata) => {
if (err) {
reject(err);
} else {
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
if (videoStream) {
console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`);
} else {
console.log('未找到视频流信息。');
}
resolve({
width: videoStream.width, height: videoStream.height,
time: parseInt(videoStream.duration),
format: metadata.format.format_name,
size,
filePath
});
}
});
})
}
export async function encodeMp4(filePath: string) {
let videoInfo = await getVideoInfo(filePath);
log("视频信息", videoInfo)
if (videoInfo.format.indexOf("mp4") === -1) {
log("视频需要转换为MP4格式", filePath)
// 转成mp4
const newPath: string = await new Promise<string>((resolve, reject) => {
const newPath = filePath + ".mp4"
ffmpeg(filePath)
.toFormat('mp4')
.on('error', (err) => {
reject(`转换视频格式失败: ${err.message}`);
})
.on('end', () => {
log('视频转换为MP4格式完成');
resolve(newPath); // 返回转换后的文件路径
})
.save(newPath);
});
return await getVideoInfo(newPath)
}
return videoInfo
}
export function isNull(value: any) {
return value === undefined || value === null;
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}

34
src/common/utils/log.ts Normal file
View File

@@ -0,0 +1,34 @@
import {selfInfo} from "../data";
import fs from "fs";
import path from "node:path";
import {DATA_DIR, truncateString} from "./index";
import {getConfigUtil} from "../config";
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
let currentDateTime = new Date().toLocaleString();
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(DATA_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
})
}

View File

@@ -0,0 +1,62 @@
import {version} from "../../version";
import https from "node:https";
export async function checkVersion() {
const latestVersionText = await getRemoteVersion();
const latestVersion = latestVersionText.split(".");
const currentVersion = version.split(".");
for (let k in [0, 1, 2]) {
if (latestVersion[k] > currentVersion[k]) {
return { result: false, version: latestVersionText };
}
}
return { result: true, version: version };
}
export async function updateLLOneBot() {
let mirrorGithubList = ["https://mirror.ghproxy.com"];
const latestVersion = await getRemoteVersion();
if (latestVersion && latestVersion != "") {
const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip";
const realUrl = mirrorGithubList[0] + downloadUrl;
}
return false;
}
export async function getRemoteVersion() {
let mirrorGithubList = ["https://521github.com"];
let Version = "";
for (let i = 0; i < mirrorGithubList.length; i++) {
let mirrorGithub = mirrorGithubList[i];
let tVersion = await getRemoteVersionByMirror(mirrorGithub);
if (tVersion && tVersion != "") {
Version = tVersion;
break;
}
}
return Version;
}
export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = "error";
let reqPromise = async function (): Promise<string> {
return new Promise((resolve, reject) => {
https.get(mirrorGithub + "/LLOneBot/LLOneBot/releases", res => {
let list = [];
res.on('data', chunk => {
list.push(chunk);
});
res.on('end', () => {
resolve(Buffer.concat(list).toString());
});
}).on('error', err => {
reject();
});
});
}
try {
releasePage = await reqPromise();
if (releasePage === "error") return "";
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0];
}
catch { }
return "";
}

View File

@@ -13,7 +13,7 @@ import {
CHANNEL_UPDATE,
} from "../common/channels";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {checkFfmpeg, checkVersion, DATA_DIR, getConfigUtil, log, updateLLOneBot} from "../common/utils";
import {DATA_DIR} from "../common/utils";
import {
friendRequests,
getFriend,
@@ -21,7 +21,7 @@ import {
getGroupMember,
llonebotError,
refreshGroupMembers,
selfInfo
selfInfo, uidMaps
} from "../common/data";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor";
@@ -41,6 +41,10 @@ import {NTQQUserApi} from "../ntqqapi/api/user";
import {NTQQGroupApi} from "../ntqqapi/api/group";
import {registerPokeHandler} from "../ntqqapi/external/ccpoke";
import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent";
import {checkVersion, updateLLOneBot} from "../common/utils/update";
import {checkFfmpeg} from "../common/utils/file";
import {log} from "../common/utils/log";
import {getConfigUtil} from "../common/config";
let running = false;
@@ -115,11 +119,18 @@ function onLoad() {
OB11Constructor.message(message).then((msg) => {
if (debug) {
msg.raw = message;
} else {
if (msg.message.length === 0) {
return
}
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin);
}
postOB11Event(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.stack.toString()));
@@ -133,17 +144,21 @@ function onLoad() {
}
async function startReceiveHook() {
registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id));
}else{
pokeEvent = new OB11FriendPokeEvent(parseInt(id));
}
postOB11Event(pokeEvent);
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
if (getConfigUtil().getConfig().enablePoke) {
registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id));
} else {
pokeEvent = new OB11FriendPokeEvent(parseInt(id));
}
postOB11Event(pokeEvent);
})
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList);
} catch (e) {
@@ -316,7 +331,13 @@ function onLoad() {
async function start() {
log("llonebot pid", process.pid)
startTime = Date.now();
dbUtil.getReceivedTempUinMap().then(m=>{
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key;
}
})
startReceiveHook().then();
NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig()

View File

@@ -1,9 +1,10 @@
import {Config} from "../common/types";
import {checkFfmpeg, getConfigUtil} from "../common/utils";
import {ob11HTTPServer} from "../onebot11/server/http";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {llonebotError} from "../common/data";
import {checkFfmpeg} from "../common/utils/file";
import {getConfigUtil} from "../common/config";
export async function setConfig(config: Config) {
let oldConfig = getConfigUtil().getConfig();

View File

@@ -9,9 +9,9 @@ import {
ElementType
} from "../types";
import path from "path";
import {log} from "../../common/utils";
import fs from "fs";
import {ReceiveCmdS} from "../hook";
import {log} from "../../common/utils/log";
export class NTQQFileApi{
static async getFileType(filePath: string) {

View File

@@ -2,9 +2,9 @@ import {ReceiveCmdS} from "../hook";
import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types";
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall";
import {uidMaps} from "../../common/data";
import {log} from "../../common/utils";
import {BrowserWindow} from "electron";
import {dbUtil} from "../../common/db";
import {log} from "../../common/utils/log";
export class NTQQGroupApi{
static async getGroups(forced = false) {

View File

@@ -1,9 +1,10 @@
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall";
import {ChatType, RawMessage, SendMessageElement} from "../types";
import {log, sleep} from "../../common/utils";
import {dbUtil} from "../../common/db";
import {selfInfo} from "../../common/data";
import {ReceiveCmdS, registerReceiveHook} from "../hook";
import {log} from "../../common/utils/log";
import {sleep} from "../../common/utils/helper";
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
@@ -19,7 +20,7 @@ export class NTQQMsgApi {
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ADD_ACTIVE_CHAT,
args: [{peer:{peerUid: groupCode, chatType: ChatType.group}, cnt: 20}]
args: [{peer:{peerUid: groupCode, chatType: ChatType.group}, cnt: 20}, null]
})
}
static async fetchRecentContact(){

View File

@@ -11,10 +11,12 @@ import {
SendTextElement,
SendVideoElement
} from "./types";
import {calculateFileMD5, encodeSilk, getVideoInfo, isGIF, log, sleep} from "../common/utils";
import {promises as fs} from "node:fs";
import ffmpeg from "fluent-ffmpeg"
import {NTQQFileApi} from "./api/file";
import {calculateFileMD5, encodeSilk, getVideoInfo, isGIF} from "../common/utils/file";
import {log} from "../common/utils/log";
import {sleep} from "../common/utils/helper";
export class SendMsgElementConstructor {

View File

@@ -1,4 +1,4 @@
import {log} from "../../../common/utils";
import {log} from "../../../common/utils/log";
let pokeEngine: any = null

View File

@@ -1,16 +1,17 @@
import {BrowserWindow} from 'electron';
import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApiClass} from "./ntcall";
import {NTQQMsgApi, sendMessagePool} from "./api/msg"
import {ChatType, Group, RawMessage, User} from "./types";
import {friends, groups, selfInfo, tempGroupCodeMap} from "../common/data";
import {friends, groups, selfInfo, tempGroupCodeMap, uidMaps} from "../common/data";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {v4 as uuidv4} from "uuid"
import {postOB11Event} from "../onebot11/server/postOB11Event";
import {HOOK_LOG} from "../common/config";
import {getConfigUtil, HOOK_LOG} from "../common/config";
import fs from "fs";
import {dbUtil} from "../common/db";
import {NTQQGroupApi} from "./api/group";
import {log} from "../common/utils/log";
import {sleep} from "../common/utils/helper";
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -249,8 +250,26 @@ registerReceiveHook<{
}
})
// 新消息
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid;
const uin = message.senderUin;
if (uid && uin) {
if (message.chatType === ChatType.temp){
dbUtil.getReceivedTempUinMap().then(receivedTempUinMap=>{
if (!receivedTempUinMap[uin]){
receivedTempUinMap[uin] = uid;
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin;
}
}
// 自动清理新消息文件
const {autoDeleteFile} = getConfigUtil().getConfig();
if (!autoDeleteFile) {
return

View File

@@ -1,8 +1,8 @@
import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log} from "../common/utils";
import {v4 as uuidv4} from "uuid"
import {log} from "../common/utils/log";
export enum NTQQApiClass {
NT_API = "ns-ntApi",

View File

@@ -1,7 +1,8 @@
import {ActionName, BaseCheckResult} from "./types"
import {OB11Response} from "./utils"
import {OB11Return} from "../types";
import {log} from "../../common/utils";
import {log} from "../../common/utils/log";
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName

View File

@@ -1,7 +1,8 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
import {log} from "../../common/utils";
import {log} from "../../common/utils/log";
interface Payload {
method: string,

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction";
import {getConfigUtil} from "../../common/utils";
import fs from "fs/promises";
import {dbUtil} from "../../common/db";
import {getConfigUtil} from "../../common/config";
export interface GetFilePayload {
file: string // 文件名

View File

@@ -4,7 +4,8 @@ import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {NTQQUserApi} from "../../ntqqapi/api/user";
import {isNull, log} from "../../common/utils";
import {log} from "../../common/utils/log";
import {isNull} from "../../common/utils/helper";
export interface PayloadType {

View File

@@ -1,7 +1,8 @@
import SendMsg from "./SendMsg";
import {ActionName, BaseCheckResult} from "./types";
import {OB11PostSendMsg} from "../types";
import {log} from "../../common/utils";
import {log} from "../../common/utils/log";
class SendGroupMsg extends SendMsg {

View File

@@ -1,8 +1,8 @@
import BaseAction from "./BaseAction";
import {getFriend, getUidByUin, uidMaps} from "../../common/data";
import {ActionName} from "./types";
import {log} from "../../common/utils";
import {NTQQFriendApi} from "../../ntqqapi/api/friend";
import {log} from "../../common/utils/log";
interface Payload {
user_id: number,

View File

@@ -7,7 +7,14 @@ import {
SendArkElement,
SendMessageElement
} from "../../ntqqapi/types";
import {friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo,} from "../../common/data";
import {
friends,
getFriend,
getGroup,
getGroupMember,
getUidByUin,
selfInfo,
} from "../../common/data";
import {
OB11MessageCustomMusic,
OB11MessageData,
@@ -22,11 +29,12 @@ import {uri2local} from "../utils";
import BaseAction from "./BaseAction";
import {ActionName, BaseCheckResult} from "./types";
import * as fs from "node:fs";
import {log, sleep} from "../../common/utils";
import {decodeCQCode} from "../cqcode";
import {dbUtil} from "../../common/db";
import {ALLOW_SEND_TEMP_MSG} from "../../common/config";
import {NTQQMsgApi} from "../../ntqqapi/api/msg";
import {log} from "../../common/utils/log";
import {sleep} from "../../common/utils/helper";
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
@@ -87,7 +95,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
if (payload.user_id && payload.message_type !== "group") {
if (!(await getFriend(payload.user_id))) {
if (!ALLOW_SEND_TEMP_MSG) {
if (!ALLOW_SEND_TEMP_MSG && !(await dbUtil.getReceivedTempUinMap())[payload.user_id.toString()]) {
return {
valid: false,
message: `不能发送临时消息`

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction";
import {log} from "../../common/utils";
import {ActionName} from "./types";
import {NTQQGroupApi} from "../../ntqqapi/api/group";
import {log} from "../../common/utils/log";
interface Payload {
group_id: number,

View File

@@ -1,8 +1,8 @@
import BaseAction from "../BaseAction";
import {Config} from "../../../common/types";
import {getConfigUtil} from "../../../common/utils";
import {ActionName} from "../types";
import {setConfig} from "../../../main/setConfig";
import {getConfigUtil} from "../../../common/config";
export class GetConfigAction extends BaseAction<null, Config> {

View File

@@ -2,9 +2,9 @@ import {GroupNotify, GroupNotifyStatus} from "../../../ntqqapi/types";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {uidMaps} from "../../../common/data";
import {log} from "../../../common/utils";
import {NTQQUserApi} from "../../../ntqqapi/api/user";
import {NTQQGroupApi} from "../../../ntqqapi/api/group";
import {log} from "../../../common/utils/log";
interface OB11GroupRequestNotify {
group_id: number,

View File

@@ -2,8 +2,8 @@ import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import { uri2local } from "../../utils";
import * as fs from "node:fs";
import { checkFileReceived } from "../../../common/utils";
import {NTQQUserApi} from "../../../ntqqapi/api/user";
import {checkFileReceived} from "../../../common/utils/file";
// import { log } from "../../../common/utils";
interface Payload {

View File

@@ -1,5 +1,6 @@
import {OB11Return} from '../types';
import {isNull} from '../../common/utils';
import {isNull} from "../../common/utils/helper";
export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {

View File

@@ -21,7 +21,6 @@ import {
User
} from '../ntqqapi/types';
import {getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data';
import {getConfigUtil, log, sleep} from "../common/utils";
import {EventType} from "./event/OB11BaseEvent";
import {encodeCQCode} from "./cqcode";
import {dbUtil} from "../common/db";
@@ -32,6 +31,9 @@ import {OB11GroupNoticeEvent} from "./event/notice/OB11GroupNoticeEvent";
import {NTQQUserApi} from "../ntqqapi/api/user";
import {NTQQFileApi} from "../ntqqapi/api/file";
import {calcQQLevel} from "../common/utils/qqlevel";
import {log} from "../common/utils/log";
import {sleep} from "../common/utils/helper";
import {getConfigUtil} from "../common/config";
export class OB11Constructor {
@@ -344,7 +346,8 @@ export class OB11Constructor {
sex: OB11Constructor.sex(member.sex),
age: 0,
area: "",
level: member.qqLevel && calcQQLevel(member.qqLevel) || 0,
level: 0,
qq_level: member.qqLevel && calcQQLevel(member.qqLevel) || 0,
join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取
title_expire_time: 0,

View File

@@ -1,8 +1,8 @@
import {Response} from "express";
import {getConfigUtil} from "../../common/utils";
import {OB11Response} from "../action/utils";
import {HttpServerBase} from "../../common/server/http";
import {actionHandlers} from "../action";
import {getConfigUtil} from "../../common/config";
class OB11HTTPServer extends HttpServerBase {
name = "OneBot V11 server"

View File

@@ -1,10 +1,11 @@
import {getConfigUtil, log} from "../../common/utils";
import {OB11Message} from "../types";
import {selfInfo} from "../../common/data";
import {OB11BaseMetaEvent} from "../event/meta/OB11BaseMetaEvent";
import {OB11BaseNoticeEvent} from "../event/notice/OB11BaseNoticeEvent";
import {WebSocket as WebSocketClass} from "ws";
import {wsReply} from "./ws/reply";
import {log} from "../../common/utils/log";
import {getConfigUtil} from "../../common/config";
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent

View File

@@ -1,6 +1,3 @@
import {getConfigUtil, log} from "../../../common/utils";
import {selfInfo} from "../../../common/data";
import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent";
import {ActionName} from "../../action/types";
@@ -11,6 +8,8 @@ import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../po
import {wsReply} from "./reply";
import {WebSocket as WebSocketClass} from "ws";
import {OB11HeartbeatEvent} from "../../event/meta/OB11HeartbeatEvent";
import {log} from "../../../common/utils/log";
import {getConfigUtil} from "../../../common/config";
export let rwsList: ReverseWebsocket[] = [];

View File

@@ -1,5 +1,4 @@
import {WebSocket} from "ws";
import {getConfigUtil, log} from "../../../common/utils";
import {actionMap} from "../../action";
import {OB11Response} from "../../action/utils";
import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postOB11Event";
@@ -11,6 +10,8 @@ import {WebsocketServerBase} from "../../../common/server/websocket";
import {IncomingMessage} from "node:http";
import {wsReply} from "./reply";
import {selfInfo} from "../../../common/data";
import {log} from "../../../common/utils/log";
import {getConfigUtil} from "../../../common/config";
let heartbeatRunning = false;

View File

@@ -1,7 +1,8 @@
import {WebSocket as WebSocketClass} from "ws";
import {OB11Response} from "../../action/utils";
import {PostEventType} from "../postOB11Event";
import {isNull, log} from "../../../common/utils";
import {log} from "../../../common/utils/log";
import {isNull} from "../../../common/utils/helper";
export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) {
try {

View File

@@ -29,6 +29,7 @@ export interface OB11GroupMember {
join_time?: number
last_sent_time?: number
level?: number
qq_level?: number
role?: OB11GroupMemberRole
title?: string
area?: string
@@ -64,6 +65,7 @@ export enum OB11MessageType {
}
export interface OB11Message {
target_id?: number; // 自己发送的消息才有此字段
self_id?: number,
time: number,
message_id: number,

View File

@@ -1,8 +1,10 @@
import {DATA_DIR, isGIF, log} from "../common/utils";
import {DATA_DIR} from "../common/utils";
import {v4 as uuidv4} from "uuid";
import * as path from 'node:path';
import * as fileType from 'file-type';
import {dbUtil} from "../common/db";
import {isGIF} from "../common/utils/file";
import {log} from "../common/utils/log";
const fs = require("fs").promises;

View File

@@ -99,6 +99,11 @@ async function onSettingWindowCreated(view: Element) {
)
]),
SettingList([
SettingItem(
'接收戳一戳消息, 暂时只支持Windows版的LLOneBot',
`重启QQ后生效如果导致QQ崩溃请勿开启此项`,
SettingSwitch('enablePoke', config.enablePoke),
),
SettingItem(
'使用 Base64 编码获取文件',
'开启后,调用 /get_image、/get_record 时,获取不到 url 时添加一个 Base64 字段',

View File

@@ -1 +1 @@
export const version = "3.16.0"
export const version = "3.16.1"