Compare commits

...

16 Commits

Author SHA1 Message Date
linyuchen
dc843f77a3 chore: ver 3.18.1 2024-03-21 18:15:08 +08:00
linyuchen
b103f2015c chore: ver 3.18.1 2024-03-21 18:14:57 +08:00
linyuchen
baf35d5496 fix: get_group_msg_history on qq version < 22106 2024-03-21 18:10:01 +08:00
linyuchen
5c34afc228 fix: audio duration 2024-03-21 13:34:49 +08:00
linyuchen
a8a6290b70 chore: ver 3.18.0 2024-03-21 13:21:08 +08:00
linyuchen
9d50c6d4fd fix: audio duration 2024-03-21 13:18:59 +08:00
linyuchen
175a8ceb3d Merge branch 'main' into dev
# Conflicts:
#	src/common/utils/file.ts
2024-03-21 13:05:15 +08:00
linyuchen
31601981f2 Merge remote-tracking branch 'origin/main' 2024-03-21 13:03:40 +08:00
linyuchen
6a8c5ec24a fix: auto create temp dir 2024-03-21 13:03:20 +08:00
linyuchen
ebca6a07c5 fix: auto create temp dir 2024-03-21 13:02:15 +08:00
linyuchen
4f9345e4e5 fix: send forward msg message param 2024-03-21 12:23:16 +08:00
linyuchen
ac17dbefe0 feat: http post secret 2024-03-21 12:21:52 +08:00
linyuchen
c9486b4f55 Merge pull request #145 from idanran/main
fix: unable to send voice in some cases
2024-03-21 10:16:27 +08:00
idanran
35951fd61a fix: unable to send voice in some cases 2024-03-20 17:32:21 +00:00
linyuchen
fdc23d7721 fix: silk duration 2024-03-20 22:47:24 +08:00
linyuchen
560428a5f9 fix: url boolean param 2024-03-20 21:00:24 +08:00
20 changed files with 83 additions and 39 deletions

View File

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

8
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1", "level": "^8.0.1",
"silk-wasm": "^3.2.3", "silk-wasm": "^3.2.4",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"
@@ -5895,9 +5895,9 @@
} }
}, },
"node_modules/silk-wasm": { "node_modules/silk-wasm": {
"version": "3.2.3", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/silk-wasm/-/silk-wasm-3.2.3.tgz", "resolved": "https://registry.npmjs.org/silk-wasm/-/silk-wasm-3.2.4.tgz",
"integrity": "sha512-zZ3hgMpiPR6cFnKvCPgPpCwx6n5RoJCbEGIFlge2kAxAmgzBTf0b2F2xIPG5W4obUhQPQXXTTH074eGZJK01xw==" "integrity": "sha512-oBkXmdIRl7cyzpoXEeEVN7v1M2yCnH1/bN8oANoYTvCqbYa5lM/CGJP47DYbpUFVO9PUpm58KP/HZaVzt4J6jw=="
}, },
"node_modules/slash": { "node_modules/slash": {
"version": "4.0.0", "version": "4.0.0",

View File

@@ -19,7 +19,7 @@
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1", "level": "^8.0.1",
"silk-wasm": "^3.2.3", "silk-wasm": "^3.2.4",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"

View File

@@ -30,6 +30,7 @@ export class ConfigUtil {
let ob11Default: OB11Config = { let ob11Default: OB11Config = {
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: "",
wsPort: 3001, wsPort: 3001,
wsHosts: [], wsHosts: [],
enableHttp: true, enableHttp: true,

View File

@@ -1,6 +1,7 @@
export interface OB11Config { export interface OB11Config {
httpPort: number httpPort: number
httpHosts: string[] httpHosts: string[]
httpSecret?: string
wsPort: number wsPort: number
wsHosts: string[] wsHosts: string[]
enableHttp?: boolean enableHttp?: boolean

View File

@@ -91,6 +91,23 @@ export async function encodeSilk(filePath: string) {
return isWav(fs.readFileSync(filePath)); return isWav(fs.readFileSync(filePath));
} }
async function guessDuration(pttPath: string){
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
function verifyDuration(oriDuration: number, guessDuration: number){
// 单位都是秒
if (oriDuration - guessDuration > 10){
return guessDuration
}
oriDuration = Math.max(1, oriDuration)
return oriDuration
}
// async function getAudioSampleRate(filePath: string) { // async function getAudioSampleRate(filePath: string) {
// try { // try {
// const mm = await import('music-metadata'); // const mm = await import('music-metadata');
@@ -104,7 +121,6 @@ export async function encodeSilk(filePath: string) {
// } // }
try { try {
const fileName = path.basename(filePath);
const pttPath = path.join(DATA_DIR, uuidv4()); const pttPath = path.join(DATA_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") { if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
@@ -118,7 +134,7 @@ export async function encodeSilk(filePath: string) {
if (ffmpegPath) { if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath); ffmpeg.setFfmpegPath(ffmpegPath);
} }
ffmpeg(filePath).toFormat("wav").audioChannels(2).on('end', function () { ffmpeg(filePath).toFormat("wav").audioChannels(1).audioFrequency(24000).on('end', function () {
log('wav转换完成'); log('wav转换完成');
}) })
.on('error', function (err) { .on('error', function (err) {
@@ -139,23 +155,22 @@ export async function encodeSilk(filePath: string) {
fs.writeFileSync(pttPath, silk.data); fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => { fs.unlink(wavPath, (err) => {
}); });
log(`语音文件${filePath}转换成功!`, pttPath) const gDuration = await guessDuration(pttPath)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
return { return {
converted: true, converted: true,
path: pttPath, path: pttPath,
duration: silk.duration, duration: verifyDuration(silk.duration / 1000, gDuration),
}; };
} else { } else {
const pcm = fs.readFileSync(filePath); const silk = fs.readFileSync(filePath);
let duration = 0; let duration = 0;
const gDuration = await guessDuration(filePath)
try { try {
duration = getDuration(pcm); duration = verifyDuration(getDuration(silk) / 1000, gDuration);
} catch (e) { } catch (e) {
log("获取语音文件时长失败", filePath, e.stack) log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s duration = gDuration;
duration = Math.floor(duration)
duration = Math.max(1, duration)
log("使用文件大小估算时长", duration)
} }
return { return {

View File

@@ -11,7 +11,7 @@ export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export const TEMP_DIR = path.join(DATA_DIR, "temp"); export const TEMP_DIR = path.join(DATA_DIR, "temp");
export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin; export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin;
if (!fs.existsSync(TEMP_DIR)) { if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR); fs.mkdirSync(TEMP_DIR, {recursive: true});
} }
export {getVideoInfo} from "./video"; export {getVideoInfo} from "./video";
export {checkFfmpeg} from "./video"; export {checkFfmpeg} from "./video";

View File

@@ -8,3 +8,5 @@ type QQPkgInfo = {
} }
export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, "app/package.json")) export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, "app/package.json"))
export const isQQ998: boolean = qqPkgInfo.buildVersion >= "22106"

View File

@@ -5,6 +5,7 @@ import {selfInfo} from "../../common/data";
import {ReceiveCmdS, registerReceiveHook} from "../hook"; import {ReceiveCmdS, registerReceiveHook} from "../hook";
import {log} from "../../common/utils/log"; import {log} from "../../common/utils/log";
import {sleep} from "../../common/utils/helper"; import {sleep} from "../../common/utils/helper";
import {isQQ998} from "../../common/utils";
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
@@ -25,7 +26,7 @@ export class NTQQMsgApi {
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number) { static async getMsgHistory(peer: Peer, msgId: string, count: number) {
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({ return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: NTQQApiMethod.HISTORY_MSG, methodName: isQQ998 ? NTQQApiMethod.HISTORY_MSG_998 : NTQQApiMethod.HISTORY_MSG,
args: [{ args: [{
peer, peer,
msgId, msgId,

View File

@@ -211,7 +211,7 @@ export class SendMsgElementConstructor {
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算 // duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration / 1000, duration: duration,
formatType: 1, formatType: 1,
voiceType: 1, voiceType: 1,
voiceChangeType: 0, voiceChangeType: 0,

View File

@@ -23,7 +23,8 @@ export enum NTQQApiClass {
export enum NTQQApiMethod { export enum NTQQApiMethod {
RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact", RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact",
ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息 ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息
HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", HISTORY_MSG_998 = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat",
HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelf",
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
SELF_INFO = "fetchAuthData", SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelBuddyService/getBuddyList", FRIENDS = "nodeIKernelBuddyService/getBuddyList",
@@ -117,7 +118,7 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
} }
const apiArgs = [methodName, ...args] const apiArgs = [methodName, ...args]
if (!cbCmd) { if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别 // QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true success = true
resolve(r) resolve(r)

View File

@@ -12,7 +12,8 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest; actionName = ActionName.SetFriendAddRequest;
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
await NTQQFriendApi.handleFriendRequest(parseInt(payload.flag), payload.approve) const approve = payload.approve.toString() === "true";
await NTQQFriendApi.handleFriendRequest(parseInt(payload.flag), approve)
return null; return null;
} }
} }

View File

@@ -16,8 +16,9 @@ export default class SetGroupAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString(); const seq = payload.flag.toString();
const approve = payload.approve.toString() === "true";
await NTQQGroupApi.handleGroupRequest(seq, await NTQQGroupApi.handleGroupRequest(seq,
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason payload.reason
) )
return null return null

View File

@@ -15,10 +15,11 @@ export default class SetGroupAdmin extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id) const member = await getGroupMember(payload.group_id, payload.user_id)
const enable = payload.enable.toString() === "true"
if (!member) { if (!member) {
throw `群成员${payload.user_id}不存在` throw `群成员${payload.user_id}不存在`
} }
await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal) await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, enable ? GroupMemberRole.admin : GroupMemberRole.normal)
return null return null
} }
} }

View File

@@ -11,8 +11,8 @@ export default class SetGroupWholeBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupWholeBan actionName = ActionName.SetGroupWholeBan
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const enable = payload.enable.toString() === "true"
await NTQQGroupApi.banGroup(payload.group_id.toString(), !!payload.enable) await NTQQGroupApi.banGroup(payload.group_id.toString(), enable)
return null return null
} }
} }

View File

@@ -11,7 +11,8 @@ import {log} from "../../../common/utils";
interface Payload { interface Payload {
group_id: number group_id: number
message_seq: number message_seq: number,
count: number
} }
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, OB11Message[]> { export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, OB11Message[]> {
@@ -24,7 +25,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, OB11
} }
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || "0" const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || "0"
// log("startMsgId", startMsgId) // log("startMsgId", startMsgId)
let msgList = (await NTQQMsgApi.getMsgHistory({chatType: ChatType.group, peerUid: group.groupCode}, startMsgId, 20)).msgList let msgList = (await NTQQMsgApi.getMsgHistory({chatType: ChatType.group, peerUid: group.groupCode}, startMsgId, parseInt(payload.count.toString()) || 0)).msgList
await Promise.all(msgList.map(async msg => { await Promise.all(msgList.map(async msg => {
msg.msgShortId = await dbUtil.addMsg(msg) msg.msgShortId = await dbUtil.addMsg(msg)
})) }))

View File

@@ -6,7 +6,9 @@ export class GoCQHTTPSendGroupForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg; actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
protected async check(payload: OB11PostSendMsg) { protected async check(payload: OB11PostSendMsg) {
payload.message = this.convertMessage2List(payload.messages); if (payload.messages){
payload.message = this.convertMessage2List(payload.messages);
}
return super.check(payload); return super.check(payload);
} }
} }

View File

@@ -6,6 +6,7 @@ import {WebSocket as WebSocketClass} from "ws";
import {wsReply} from "./ws/reply"; import {wsReply} from "./ws/reply";
import {log} from "../../common/utils/log"; import {log} from "../../common/utils/log";
import {getConfigUtil} from "../../common/config"; import {getConfigUtil} from "../../common/config";
import crypto from 'crypto';
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
@@ -39,18 +40,26 @@ export function postOB11Event(msg: PostEventType, reportSelf = false) {
} }
} }
if (config.ob11.enableHttpPost) { if (config.ob11.enableHttpPost) {
const msgStr = JSON.stringify(msg);
const hmac = crypto.createHmac('sha1', config.ob11.httpSecret);
hmac.update(msgStr);
const sig = hmac.digest('hex');
let headers = {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
}
if (config.ob11.httpSecret) {
headers["x-signature"] = "sha1=" + sig;
}
for (const host of config.ob11.httpHosts) { for (const host of config.ob11.httpHosts) {
fetch(host, { fetch(host, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/json", body: msgStr
"x-self-id": selfInfo.uin
},
body: JSON.stringify(msg)
}).then((res: any) => { }).then((res: any) => {
log(`新消息事件HTTP上报成功: ${host} ` + JSON.stringify(msg)); log(`新消息事件HTTP上报成功: ${host} ` + msgStr);
}, (err: any) => { }, (err: any) => {
log(`新消息事件HTTP上报失败: ${host} ` + err + JSON.stringify(msg)); log(`新消息事件HTTP上报失败: ${host} `, err, msg);
}); });
} }
} }

View File

@@ -55,6 +55,14 @@ async function onSettingWindowCreated(view: Element) {
SettingSwitch('ob11.enableHttpPost', config.ob11.enableHttpPost, {'control-display-id': 'config-ob11-httpHosts'}), SettingSwitch('ob11.enableHttpPost', config.ob11.enableHttpPost, {'control-display-id': 'config-ob11-httpHosts'}),
), ),
`<div class="config-host-list" id="config-ob11-httpHosts" ${config.ob11.enableHttpPost ? '' : 'is-hidden'}> `<div class="config-host-list" id="config-ob11-httpHosts" ${config.ob11.enableHttpPost ? '' : 'is-hidden'}>
<setting-item data-direction="row">
<div>
<setting-text>HTTP 事件上报密钥</setting-text>
</div>
<div class="q-input">
<input id="config-ob11-httpSecret" class="q-input__inner" data-config-key="ob11.httpSecret" type="text" value="${config.ob11.httpSecret}" placeholder="未设置" />
</div>
</setting-item>
<setting-item data-direction="row"> <setting-item data-direction="row">
<div> <div>
<setting-text>HTTP 事件上报地址</setting-text> <setting-text>HTTP 事件上报地址</setting-text>

View File

@@ -1 +1 @@
export const version = "3.17.0" export const version = "3.18.1"