Compare commits

...

9 Commits

Author SHA1 Message Date
linyuchen
959eab441e Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-04-06 23:58:27 +08:00
linyuchen
441c0c6946 feat: @全体的时候判断剩余次数 2024-04-06 23:57:07 +08:00
linyuchen
240cdade07 fix: getFriend 2024-04-06 23:30:40 +08:00
linyuchen
0132d97bd9 Merge pull request #177 from idanran/main
fix: audio may fail to convert
2024-04-04 12:37:37 +08:00
linyuchen
b34c7f045c fix: at all when member isn't admin 2024-04-04 12:26:28 +08:00
idanran
ab91313e69 fix 2024-04-04 04:22:56 +00:00
idanran
1f8966aaf4 fix: audio may fail to convert 2024-04-04 02:08:08 +00:00
linyuchen
ec073da3f6 feat: 发送戳一戳 2024-04-03 00:03:50 +08:00
linyuchen
80131e0472 fix: send msg auto_escape 2024-04-02 12:51:08 +08:00
16 changed files with 186 additions and 97 deletions

View File

@@ -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/'},
]
})]
},

View File

@@ -1,10 +1,10 @@
{
"manifest_version": 4,
"type": "extension",
"name": "LLOneBot v3.20.7",
"name": "LLOneBot v3.21.0",
"slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
"version": "3.20.7",
"version": "3.21.0",
"icon": "./icon.jpg",
"authors": [
{

View File

@@ -28,7 +28,7 @@ export const llonebotError: LLOneBotError = {
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid) ? "uin" : "uid"
let filterKey = isNumeric(uinOrUid.toString()) ? "uin" : "uid"
let filterValue = uinOrUid
let friend = friends.find(friend => friend[filterKey] === filterValue.toString())
// if (!friend) {

View File

@@ -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<Buffer>((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) {

View File

@@ -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) {

View File

@@ -186,6 +186,17 @@ export class NTQQGroupApi{
})
}
static async getGroupAtAllRemainCount(groupCode: string){
return await callNTQQApi<GeneralCallResult & {"atInfo":{"canAtAll": boolean,"RemainAtAllCountForUin": number,"RemainAtAllCountForGroup": number,"atTimesMsg": string,"canNotAtAllMsg":""}}>({
methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT,
args: [
{
groupCode
}, null
]
})
}
// 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({

View File

@@ -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,

View File

@@ -1,28 +0,0 @@
import {log} from "../../../common/utils/log";
let pokeEngine: any = null
type PokeHandler = (id: string, isGroup: boolean)=>void
let pokeRecords: Record<string, number> = {}
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);
})
}

Binary file not shown.

Binary file not shown.

53
src/ntqqapi/external/crychic/index.ts vendored Normal file
View File

@@ -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<string, number> = {}
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()

View File

@@ -50,6 +50,7 @@ export enum NTQQApiMethod {
GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
GROUP_AT_ALL_REMAIN_COUNT = "nodeIKernelGroupService/getGroupRemainAtTimes",
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest",
KICK_MEMBER = "nodeIKernelGroupService/kickMember",
@@ -77,7 +78,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 +197,15 @@ export class NTQQApi {
]
})
}
static async fetchUnitedCommendConfig() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
args:[
{
groups: ['100243']
}
]
})
}
}

View File

@@ -2,19 +2,15 @@ import {
AtType,
ChatType,
ElementType,
Group, PicSubType,
Friend,
Group,
GroupMemberRole,
PicSubType,
RawMessage,
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,
@@ -23,7 +19,7 @@ import {
OB11MessageNode,
OB11PostSendMsg
} from '../../types';
import {Peer} from "../../../ntqqapi/api/msg";
import {NTQQMsgApi, Peer} from "../../../ntqqapi/api/msg";
import {SendMsgElementConstructor} from "../../../ntqqapi/constructor";
import BaseAction from "../BaseAction";
import {ActionName, BaseCheckResult} from "../types";
@@ -31,10 +27,11 @@ import * as fs from "node:fs";
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";
import {uri2local} from "../../../common/utils";
import {crychic} from "../../../ntqqapi/external/crychic";
import {NTQQGroupApi} from "../../../ntqqapi/api";
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
@@ -93,7 +90,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,17 +106,31 @@ export async function createSendElements(messageData: OB11MessageData[], group:
}
break;
case OB11MessageDataType.at: {
if (!group) {
if (!target) {
continue
}
let atQQ = sendMsg.data?.qq;
if (atQQ) {
atQQ = atQQ.toString()
if (atQQ === "all") {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, "全体成员"))
// todo查询剩余的at全体次数
const groupCode = (target as Group)?.groupCode;
let remainAtAllCount = 1
let isAdmin: boolean = true;
if (groupCode) {
try {
remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo.RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount);
const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin);
isAdmin = self.role === GroupMemberRole.admin || self.role === GroupMemberRole.owner;
} catch (e) {}
}
if(isAdmin && remainAtAllCount > 0) {
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 +208,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 +258,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
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 +287,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
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 +296,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
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
@@ -294,7 +321,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} else {
throw ("发送消息参数错误, 请指定group_id或user_id")
}
const messages = convertMessage2List(payload.message);
const messages = convertMessage2List(payload.message, !!payload.auto_escape);
if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
@@ -318,7 +345,12 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
}
// 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 +510,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement {
const musicJson = {
app: 'com.tencent.structmsg',

View File

@@ -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"
@@ -225,6 +234,7 @@ export interface OB11PostSendMsg {
group_id?: string,
message: OB11MessageMixType;
messages?: OB11MessageMixType; // 兼容 go-cqhttp
auto_escape?: boolean
}
export interface OB11Version {

View File

@@ -133,8 +133,8 @@ async function onSettingWindowCreated(view: Element) {
]),
SettingList([
SettingItem(
'接收戳一戳消息, 暂时只支持Windows版的LLOneBot',
`重启QQ后生效如果导致QQ崩溃请勿开启此项`,
'戳一戳消息, 暂时只支持Windows版的LLOneBot',
`重启QQ后生效如果导致QQ崩溃请勿开启此项, 群戳一戳只能收到群号`,
SettingSwitch('enablePoke', config.enablePoke),
),
SettingItem(

View File

@@ -1 +1 @@
export const version = "3.20.7"
export const version = "3.21.0"