diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 1139273..ae6ddfd 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -32,7 +32,7 @@ let config = { targets: [ ...external.map(genCpModule), {src: './manifest.json', dest: 'dist'}, {src: './icon.jpg', dest: 'dist'}, - {src: './src/ntqqapi/external/ccpoke/poke-win32-x64.node', dest: 'dist/main/ccpoke/'}, + {src: './src/ntqqapi/external/crychic/crychic-win32-x64.node', dest: 'dist/main/'}, ] })] }, diff --git a/src/common/utils/audio.ts b/src/common/utils/audio.ts index be6e876..bd87ecc 100644 --- a/src/common/utils/audio.ts +++ b/src/common/utils/audio.ts @@ -6,7 +6,7 @@ import path from "node:path"; import {DATA_DIR, TEMP_DIR} from "./index"; import {v4 as uuidv4} from "uuid"; import {getConfigUtil} from "../config"; -import ffmpeg from "fluent-ffmpeg"; +import {spawn} from "node:child_process" export async function encodeSilk(filePath: string) { function getFileHeader(filePath: string) { @@ -64,50 +64,44 @@ export async function encodeSilk(filePath: string) { if (getFileHeader(filePath) !== "02232153494c4b") { log(`语音文件${filePath}需要转换成silk`) const _isWav = await isWavFile(filePath); - const wavPath = pttPath + ".wav" - const convert = async () => { - return await new Promise((resolve, reject) => { - const ffmpegPath = getConfigUtil().getConfig().ffmpeg; - if (ffmpegPath) { - ffmpeg.setFfmpegPath(ffmpegPath); - } - ffmpeg(filePath).toFormat("wav") - .audioChannels(1) - .audioFrequency(24000) - .on('end', function () { - log('wav转换完成'); + const pcmPath = pttPath + ".pcm" + let sampleRate = 0 + const convert = () => { + return new Promise((resolve, reject) => { + const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || "ffmpeg" + const cp = spawn(ffmpegPath, ["-y", "-i", filePath, "-ar", "24000", "-ac", "1", "-f", "s16le", pcmPath]) + cp.on("error", err => { + log(`FFmpeg处理转换出错: `, err.message) + return reject(err) + }) + cp.on("exit", (code, signal) => { + const EXIT_CODES = [0, 255] + if (code == null || EXIT_CODES.includes(code)) { + sampleRate = 24000 + const data = fs.readFileSync(pcmPath) + fs.unlink(pcmPath, (err) => { + }) + return resolve(data) + } + log(`FFmpeg exit: code=${code ?? "unknown"} sig=${signal ?? "unknown"}`) + reject(Error(`FFmpeg处理转换失败`)) }) - .on('error', function (err) { - log(`wav转换出错: `, err.message,); - reject(err); - }) - .save(wavPath) - .on("end", () => { - filePath = wavPath - resolve(wavPath); - }); }) } - let wav: Buffer + let input: Buffer if (!_isWav) { - log(`语音文件${filePath}正在转换成wav`) - await convert() + input = await convert() } else { - wav = fs.readFileSync(filePath) + input = fs.readFileSync(filePath) const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] - const {fmt} = getWavFileInfo(wav) + const {fmt} = getWavFileInfo(input) // log(`wav文件信息`, fmt) if (!allowSampleRate.includes(fmt.sampleRate)) { - wav = undefined - await convert() + input = await convert() } } - wav ||= fs.readFileSync(filePath); - const silk = await encode(wav, 0); + const silk = await encode(input, sampleRate); fs.writeFileSync(pttPath, silk.data); - fs.unlink(wavPath, (err) => { - }); - // const gDuration = await guessDuration(pttPath) log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration) return { converted: true, @@ -127,7 +121,7 @@ export async function encodeSilk(filePath: string) { return { converted: false, path: filePath, - duration: duration, + duration, }; } } catch (error) { diff --git a/src/main/main.ts b/src/main/main.ts index dd5e047..005aa58 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -47,7 +47,7 @@ import {dbUtil} from "../common/db"; import {setConfig} from "./setConfig"; import {NTQQUserApi} from "../ntqqapi/api/user"; import {NTQQGroupApi} from "../ntqqapi/api/group"; -import {registerPokeHandler} from "../ntqqapi/external/ccpoke"; +import {crychic} from "../ntqqapi/external/crychic"; import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent"; import {checkNewVersion, upgradeLLOneBot} from "../common/utils/upgrade"; import {log} from "../common/utils/log"; @@ -183,7 +183,8 @@ function onLoad() { async function startReceiveHook() { if (getConfigUtil().getConfig().enablePoke) { - registerPokeHandler((id, isGroup) => { + crychic.loadNode() + crychic.registerPokeHandler((id, isGroup) => { log(`收到戳一戳消息了!是否群聊:${isGroup},id:${id}`) let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent; if (isGroup) { diff --git a/src/ntqqapi/constructor.ts b/src/ntqqapi/constructor.ts index 76e3368..ad802c2 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -21,6 +21,10 @@ import {encodeSilk} from "../common/utils/audio"; export class SendMsgElementConstructor { + + static poke(groupCode: string, uin: string){ + return null + } static text(content: string): SendTextElement { return { elementType: ElementType.TEXT, diff --git a/src/ntqqapi/external/ccpoke/index.ts b/src/ntqqapi/external/ccpoke/index.ts deleted file mode 100644 index 011b17c..0000000 --- a/src/ntqqapi/external/ccpoke/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {log} from "../../../common/utils/log"; - -let pokeEngine: any = null - -type PokeHandler = (id: string, isGroup: boolean)=>void - -let pokeRecords: Record = {} -export function registerPokeHandler(handler: PokeHandler){ - if(!pokeEngine){ - try { - pokeEngine = require("./ccpoke/poke-win32-x64.node") - pokeEngine.performHooks(); - }catch (e) { - log("戳一戳引擎加载失败", e) - return - } - } - pokeEngine.setHandlerForPokeHook((id: string, isGroup: boolean)=>{ - let existTime = pokeRecords[id] - if (existTime){ - if (Date.now() - existTime < 1500){ - return - } - } - pokeRecords[id] = Date.now() - handler(id, isGroup); - }) -} \ No newline at end of file diff --git a/src/ntqqapi/external/ccpoke/poke-win32-x64.node b/src/ntqqapi/external/ccpoke/poke-win32-x64.node deleted file mode 100644 index 581f88e..0000000 Binary files a/src/ntqqapi/external/ccpoke/poke-win32-x64.node and /dev/null differ diff --git a/src/ntqqapi/external/crychic/crychic-win32-x64.node b/src/ntqqapi/external/crychic/crychic-win32-x64.node new file mode 100644 index 0000000..2f3e843 Binary files /dev/null and b/src/ntqqapi/external/crychic/crychic-win32-x64.node differ diff --git a/src/ntqqapi/external/crychic/index.ts b/src/ntqqapi/external/crychic/index.ts new file mode 100644 index 0000000..f090c01 --- /dev/null +++ b/src/ntqqapi/external/crychic/index.ts @@ -0,0 +1,53 @@ +import {log} from "../../../common/utils"; +import {NTQQApi} from "../../ntcall"; + +type PokeHandler = (id: string, isGroup: boolean) => void +type CrychicHandler = (event: string, id: string, isGroup: boolean) => void + +let pokeRecords: Record = {} + +class Crychic{ + private crychic: any = undefined + + loadNode(){ + if (!this.crychic){ + try { + this.crychic = require("./crychic-win32-x64.node") + this.crychic.init() + }catch (e) { + log("crychic加载失败", e) + } + + } + } + registerPokeHandler(fn: PokeHandler){ + this.registerHandler((event, id, isGroup)=>{ + if (event === "poke"){ + let existTime = pokeRecords[id] + if (existTime) { + if (Date.now() - existTime < 1500) { + return + } + } + pokeRecords[id] = Date.now() + fn(id, isGroup); + } + }) + } + registerHandler(fn: CrychicHandler){ + if (!this.crychic) return; + this.crychic.setCryHandler(fn) + } + sendFriendPoke(friendUid: string){ + if (!this.crychic) return; + this.crychic.sendFriendPoke(parseInt(friendUid)) + NTQQApi.fetchUnitedCommendConfig().then() + } + sendGroupPoke(groupCode: string, memberUin: string){ + if (!this.crychic) return; + this.crychic.sendGroupPoke(parseInt(memberUin), parseInt(groupCode)) + NTQQApi.fetchUnitedCommendConfig().then() + } +} + +export const crychic = new Crychic() \ No newline at end of file diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index ee29eec..59b082d 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -77,7 +77,9 @@ export enum NTQQApiMethod { SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', GET_SKEY = "nodeIKernelTipOffService/getPskey", - UPDATE_SKEY = "updatePskey" + UPDATE_SKEY = "updatePskey", + + FETCH_UNITED_COMMEND_CONFIG = "nodeIKernelUnitedConfigService/fetchUnitedCommendConfig" // 发包需要调用的 } enum NTQQApiChannel { @@ -194,4 +196,15 @@ export class NTQQApi { ] }) } + + static async fetchUnitedCommendConfig() { + return await callNTQQApi({ + methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG, + args:[ + { + groups: ['100243'] + } + ] + }) + } } \ No newline at end of file diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index 958be8d..2b98c41 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -1,7 +1,7 @@ import { AtType, ChatType, - ElementType, + ElementType, Friend, Group, PicSubType, RawMessage, SendArkElement, @@ -35,6 +35,7 @@ import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; import {log} from "../../../common/utils/log"; import {sleep} from "../../../common/utils/helper"; import {uri2local} from "../../../common/utils"; +import {crychic} from "../../../ntqqapi/external/crychic"; function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkUri(uri: string): boolean { @@ -93,7 +94,7 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa return message; } -export async function createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) { +export async function createSendElements(messageData: OB11MessageData[], target: Group | Friend | undefined, ignoreTypes: OB11MessageDataType[] = []) { let sendElements: SendMessageElement[] = [] let deleteAfterSentFiles: string[] = [] for (let sendMsg of messageData) { @@ -109,7 +110,7 @@ export async function createSendElements(messageData: OB11MessageData[], group: } break; case OB11MessageDataType.at: { - if (!group) { + if (!target) { continue } let atQQ = sendMsg.data?.qq; @@ -119,7 +120,7 @@ export async function createSendElements(messageData: OB11MessageData[], group: sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, "全体成员")) } else { // const atMember = group?.members.find(m => m.uin == atQQ) - const atMember = await getGroupMember(group?.groupCode, atQQ); + const atMember = await getGroupMember((target as Group)?.groupCode, atQQ); if (atMember) { sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick)) } @@ -197,7 +198,22 @@ export async function createSendElements(messageData: OB11MessageData[], group: case OB11MessageDataType.json: { sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data)) } - break + break; + case OB11MessageDataType.poke: { + let qq = sendMsg.data?.qq || sendMsg.data?.id + if (qq) { + if ("groupCode" in target) { + crychic.sendGroupPoke(target.groupCode, qq.toString()) + } else { + if (!qq) { + qq = parseInt(target.uin) + } + crychic.sendFriendPoke(qq.toString()) + } + sendElements.push(SendMsgElementConstructor.poke("", "")) + } + } + break; } } @@ -232,7 +248,7 @@ export class SendMsg extends BaseAction { message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素" } } - if (payload.message_type !== "private" && payload.group_id &&!(await getGroup(payload.group_id))) { + if (payload.message_type !== "private" && payload.group_id && !(await getGroup(payload.group_id))) { return { valid: false, message: `群${payload.group_id}不存在` @@ -261,6 +277,7 @@ export class SendMsg extends BaseAction { } let isTempMsg = false; let group: Group | undefined = undefined; + let friend: Friend | undefined = undefined; const genGroupPeer = async () => { group = await getGroup(payload.group_id.toString()) peer.chatType = ChatType.group @@ -269,7 +286,7 @@ export class SendMsg extends BaseAction { } const genFriendPeer = () => { - const friend = friends.find(f => f.uin == payload.user_id.toString()) + friend = friends.find(f => f.uin == payload.user_id.toString()) if (friend) { // peer.name = friend.nickName peer.peerUid = friend.uid @@ -318,7 +335,12 @@ export class SendMsg extends BaseAction { } } // log("send msg:", peer, sendElements) - const {sendElements, deleteAfterSentFiles} = await createSendElements(messages, group) + const {sendElements, deleteAfterSentFiles} = await createSendElements(messages, group || friend) + if (sendElements.length === 1){ + if (sendElements[0] === null){ + return {message_id: 0} + } + } const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles) deleteAfterSentFiles.map(f => fs.unlink(f, () => { })); @@ -478,8 +500,6 @@ export class SendMsg extends BaseAction { } - - private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement { const musicJson = { app: 'com.tencent.structmsg', diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index 3b6f249..289f959 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -116,7 +116,8 @@ export enum OB11MessageDataType { markdown = "markdown", node = "node", // 合并转发消息节点 forward = "forward", // 合并转发消息,用于上报 - xml = "xml" + xml = "xml", + poke = "poke" } export interface OB11MessageMFace{ @@ -132,6 +133,14 @@ export interface OB11MessageText { } } +export interface OB11MessagePoke{ + type: OB11MessageDataType.poke + data: { + qq?: number, + id?: number + } +} + interface OB11MessageFileBase { data: { thumb?: string; @@ -217,7 +226,7 @@ export type OB11MessageData = OB11MessageFace | OB11MessageMFace | OB11MessageAt | OB11MessageReply | OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo | - OB11MessageNode | OB11MessageCustomMusic | OB11MessageJson + OB11MessageNode | OB11MessageCustomMusic | OB11MessageJson | OB11MessagePoke export interface OB11PostSendMsg { message_type?: "private" | "group"