Compare commits

..

49 Commits

Author SHA1 Message Date
linyuchen
bc4511e175 chore: v3.27.2 2024-07-16 21:43:50 +08:00
linyuchen
f191103f99 Merge pull request #294 from cnxysoft/dev
修复戳一戳
2024-07-16 21:38:17 +08:00
linyuchen
408463f63b Merge branch 'dev' into dev 2024-07-16 21:21:50 +08:00
Alen
fb96c4272e 修复戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 21:01:19 +08:00
linyuchen
1dd468e2ff fix: #290 2024-07-13 16:25:00 +08:00
linyuchen
2a1aa8c649 feat: image subType 2024-07-13 14:26:23 +08:00
linyuchen
1633734e08 Merge branch 'dev' 2024-07-13 14:09:45 +08:00
linyuchen
dff92e6f27 chore: version 3.27.0
feat: support poke
feat: LLOneBot global switch
2024-07-13 14:09:03 +08:00
linyuchen
dba5e30d5d doc: plugin description 2024-07-10 13:48:05 +08:00
linyuchen
2d04ab2e72 fix: crychic crash 2024-07-10 13:47:44 +08:00
linyuchen
1a015ac8d3 Merge pull request #262 from LLOneBot/dev
get_record 支持 out_format 进行转码,和其他小修复
2024-06-21 17:39:53 +08:00
linyuchen
6390620ddd chore: version 3.26.7 2024-06-21 17:33:48 +08:00
linyuchen
0d19005dc3 refactor: remove duplicate import 2024-06-21 17:28:17 +08:00
linyuchen
c6479dd2c4 Merge remote-tracking branch 'origin/dev' into dev 2024-06-21 16:21:15 +08:00
linyuchen
8871331b7c 🐛 fix: ws echo #261 2024-06-21 16:20:59 +08:00
linyuchen
e01148b86a 🐛 fix: ws echo 2024-06-21 16:20:26 +08:00
linyuchen
2f87e3818e Merge pull request #260 from idranme/main
perf: audio
2024-06-21 10:36:29 +08:00
linyuchen
2c8a594c38 Merge branch 'dev' into main 2024-06-21 10:36:14 +08:00
idranme
1508dab7fe perf: audio 2024-06-18 19:15:56 +00:00
linyuchen
958b21e47e fix: wait get_file download complete 2024-06-17 17:41:23 +08:00
linyuchen
781c3311ae fix: get_file cache not found 2024-06-17 16:20:37 +08:00
linyuchen
52850d172e feat: decode silk 2024-06-17 16:05:38 +08:00
linyuchen
52a065542e chore: v3.26.6 2024-06-10 14:38:20 +08:00
linyuchen
fd10469685 feat: video url 2024-06-10 14:35:00 +08:00
linyuchen
a2ee75b113 refactor: sent msg status waiter 2024-06-09 15:27:33 +08:00
linyuchen
0f7f243b98 Merge pull request #250 from Bluefissure/reverse-ws-ua
feat: add ua to reverse websocket headers
2024-06-06 17:35:21 +08:00
Bluefissure
97d7996a50 fix: add version to ua 2024-06-06 08:53:37 +00:00
Bluefissure
b658d164f9 feat: add ua to reverse websocket headers 2024-06-06 08:48:18 +00:00
linyuchen
f150ae478b chore: v3.26.5 2024-06-01 20:19:05 +08:00
linyuchen
d1f68553f1 fix: 加载卡顿,群成员名片变动 2024-06-01 20:18:38 +08:00
linyuchen
f47f0800de Merge remote-tracking branch 'origin/main' 2024-05-29 16:56:08 +08:00
linyuchen
b7ddefc950 fix: QZone cookies 2024-05-29 16:38:22 +08:00
linyuchen
25b3325a44 fix: comment 2024-05-29 16:28:46 +08:00
linyuchen
c281b87bab merge main 2024-05-29 16:27:06 +08:00
linyuchen
c0946ddda2 chore: version 3.26.4 2024-05-29 16:26:04 +08:00
linyuchen
1128cf679c refactor: send file timeout 2024-05-29 16:25:42 +08:00
linyuchen
ff65a42350 Merge pull request #242 from LLOneBot/dev
feat: support qzone cookies
2024-05-29 16:24:32 +08:00
手瓜一十雪
c459587dcd refactor: get cookies 2024-05-29 12:03:35 +08:00
手瓜一十雪
6f8ea9677f feat: support qzone cookies 2024-05-28 17:14:24 +08:00
手瓜一十雪
38197527fa Merge branch 'main' into dev 2024-05-28 17:11:13 +08:00
手瓜一十雪
21b2bd2c8e feat: cookies 2024-05-28 17:11:07 +08:00
linyuchen
25158eee55 chore: version 3.26.3 2024-05-28 16:41:28 +08:00
linyuchen
1aa804f255 chore: version 3.26.3 2024-05-28 16:41:22 +08:00
linyuchen
fbe101339d fix: #237 2024-05-28 16:40:51 +08:00
linyuchen
a4aeb8171d fix: QQ package.json on macOS 2024-05-28 15:42:22 +08:00
linyuchen
27f98a459c fix: member info change on version 24108 2024-05-28 15:31:59 +08:00
linyuchen
e6b0eaa46d Merge pull request #235 from LLOneBot/dev
快速操作回复自动引用原消息开关
2024-05-24 17:14:54 +08:00
linyuchen
f336317a33 chore: version 3.26.2 2024-05-24 17:12:35 +08:00
linyuchen
17b44cc0fa refactor: #226 Quick operation reply automatically quotes the original message switch 2024-05-24 17:10:41 +08:00
31 changed files with 697 additions and 371 deletions

View File

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

View File

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

View File

@@ -40,8 +40,10 @@ export class ConfigUtil {
enableWsReverse: false,
messagePostFormat: 'array',
enableHttpHeart: false,
enableQOAutoQuote: false
}
let defaultConfig: Config = {
enableLLOB: true,
ob11: ob11Default,
heartInterval: 60000,
token: '',

View File

@@ -10,12 +10,14 @@ export interface OB11Config {
enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
}
export interface CheckVersion {
result: boolean
version: string
}
export interface Config {
enableLLOB: boolean
ob11: OB11Config
token?: string
heartInterval?: number // ms

View File

@@ -3,26 +3,34 @@ import fs from 'node:fs'
import os from 'node:os'
import { systemPlatform } from './system'
export const exePath = process.execPath;
export const exePath = process.execPath
export const pkgInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'package.json');
let configVersionInfoPath;
function getPKGPath() {
let p = path.join(path.dirname(exePath), 'resources', 'app', 'package.json')
if (systemPlatform === 'darwin') {
p = path.join(path.dirname(path.dirname(exePath)), 'Resources', 'app', 'package.json')
}
return p
}
export const pkgInfoPath = getPKGPath()
let configVersionInfoPath: string
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json');
} else {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './.config/QQ');
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json')
}
else {
const userPath = os.homedir()
const appDataPath = path.resolve(userPath, './.config/QQ')
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json')
}
if (typeof configVersionInfoPath !== 'string') {
throw new Error('Something went wrong when load QQ info path');
throw new Error('Something went wrong when load QQ info path')
}
export { configVersionInfoPath };
export { configVersionInfoPath }
type QQPkgInfo = {
version: string;
@@ -43,21 +51,21 @@ let _qqVersionConfigInfo: QQVersionConfigInfo = {
'curVersion': '9.9.9-23361',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '23361'
};
'buildId': '23361',
}
if (fs.existsSync(configVersionInfoPath)) {
try {
const _ =JSON.parse(fs.readFileSync(configVersionInfoPath).toString());
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _);
const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString())
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _)
} catch (e) {
console.error('Load QQ version config info failed, Use default version', e);
console.error('Load QQ version config info failed, Use default version', e)
}
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo;
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath);
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.9-23159',
@@ -66,10 +74,10 @@ export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath);
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
let _appid: string = '537213803'; // 默认为 Windows 平台的 appid
let _appid: string = '537213803' // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') {
_appid = '537213827';
_appid = '537213827'
}
// todo: mac 平台的 appid
export const appid = _appid;
export const appid = _appid
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

View File

@@ -1,9 +1,9 @@
import fs from 'fs'
import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm'
import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log'
import path from 'node:path'
import { DATA_DIR, TEMP_DIR } from './index'
import { TEMP_DIR } from './index'
import { v4 as uuidv4 } from 'uuid'
import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process'
@@ -60,10 +60,11 @@ export async function encodeSilk(filePath: string) {
// }
try {
const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, uuidv4())
if (getFileHeader(filePath) !== '02232153494c4b') {
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath)
const _isWav = isWav(file)
const pcmPath = pttPath + '.pcm'
let sampleRate = 0
const convert = () => {
@@ -79,7 +80,8 @@ export async function encodeSilk(filePath: string) {
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000
const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {})
fs.unlink(pcmPath, (err) => {
})
return resolve(data)
}
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
@@ -91,7 +93,7 @@ export async function encodeSilk(filePath: string) {
if (!_isWav) {
input = await convert()
} else {
input = fs.readFileSync(filePath)
input = file
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt)
@@ -108,7 +110,7 @@ export async function encodeSilk(filePath: string) {
duration: silk.duration / 1000,
}
} else {
const silk = fs.readFileSync(filePath)
const silk = file
let duration = 0
try {
duration = getDuration(silk) / 1000
@@ -128,3 +130,41 @@ export async function encodeSilk(filePath: string) {
return {}
}
}
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') {
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath))
const outPCMPath = fileName + '.pcm'
const outFilePath = fileName + '.' + outFormat
await fsPromise.writeFile(outPCMPath, data)
const convert = () => {
return new Promise<string>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, [
'-y',
'-f', 's16le', // PCM format
'-ar', '24000', // Sample rate
'-ac', '1', // Number of audio channels
'-i', outPCMPath,
outFilePath,
])
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)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
}

View File

@@ -1,30 +1,50 @@
import https from 'node:https';
import http from 'node:http';
import { log } from '@/common/utils/log'
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<Map<string, string>> {
return new Promise<Map<string, string>>((resolve, reject) => {
const protocol = url.startsWith('https://') ? https : http;
protocol.get(url, (res) => {
const cookiesHeader = res.headers['set-cookie'];
if (!cookiesHeader) {
resolve(new Map<string, string>());
} else {
const cookiesMap = new Map<string, string>();
cookiesHeader.forEach((cookieStr) => {
cookieStr.split(';').forEach((cookiePart) => {
const trimmedPart = cookiePart.trim();
if (trimmedPart.includes('=')) {
const [key, value] = trimmedPart.split('=').map(part => part.trim());
cookiesMap.set(key, decodeURIComponent(value)); // 解码cookie值
}
});
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
client.get(url, (res) => {
let cookies: { [key: string]: string } = {};
const handleRedirect = (res: http.IncomingMessage) => {
//console.log(res.headers.location);
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies
log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies };
resolve(cookies);
});
} else {
resolve(cookies);
}
} else {
resolve(cookies);
}
};
res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
handleRedirect(res);
});
if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']);
log('set-cookie', url, res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('=');
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
}
});
resolve(cookiesMap);
}
}).on('error', (error) => {
reject(error);
}).on('error', (err) => {
reject(err);
});
});
}

View File

@@ -13,7 +13,7 @@ import {
CHANNEL_UPDATE,
} from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR } from '../common/utils'
import { DATA_DIR, qqPkgInfo } from '../common/utils'
import {
friendRequests,
getFriend,
@@ -25,7 +25,7 @@ import {
selfInfo,
uidMaps,
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook } from '../ntqqapi/hook'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import {
ChatType,
@@ -56,6 +56,7 @@ import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/native/wrapper'
import { sentMessages } from '@/ntqqapi/api'
let running = false
@@ -190,29 +191,36 @@ function onLoad() {
postOb11Event(groupEvent)
}
})
OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
if (friendAddEvent) {
// log("post friend add event", friendAddEvent);
postOb11Event(friendAddEvent)
OB11Constructor.PrivateEvent(message).then((privateEvent) => {
log(message)
if (privateEvent) {
// log("post private event", privateEvent);
postOb11Event(privateEvent)
}
})
}
}
async function startReceiveHook() {
startHook().then()
if (getConfigUtil().getConfig().enablePoke) {
crychic.loadNode()
crychic.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)
})
if ( qqPkgInfo.buildVersion > '23873'){
log(`当前版本${qqPkgInfo.buildVersion}不支持发送戳一戳模块`)
}
else {
crychic.loadNode()
crychic.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>
@@ -226,6 +234,10 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
const sentMessage = sentMessages[message.msgId]
if (sentMessage){
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message)
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
@@ -349,32 +361,48 @@ function onLoad() {
log('获取群通知的成员信息失败', notify, e.stack.toString())
}
}
else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求')
let groupRequestEvent = new OB11GroupRequestEvent()
groupRequestEvent.group_id = parseInt(notify.group.groupCode)
let requestQQ = ''
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
let requestQQ = uidMaps[notify.user1.uid]
if (!requestQQ) {
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
}
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0
groupRequestEvent.sub_type = 'add'
groupRequestEvent.comment = notify.postscript
groupRequestEvent.flag = notify.seq
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid]
if (!invitorQQ) {
try {
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))
groupRequestEvent.invitor_id = parseInt(invitor.uin)
} catch (e) {
groupRequestEvent.invitor_id = 0
log('获取邀请人QQ号失败', e)
}
}
}
postOb11Event(groupRequestEvent)
}
else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
let groupInviteEvent = new OB11GroupRequestEvent()
groupInviteEvent.group_id = parseInt(notify.group.groupCode)
let user_id = (await getFriend(notify.user2.uid))?.uin
let user_id = uidMaps[notify.user2.uid]
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id)
groupInviteEvent.sub_type = 'invite'
// groupInviteEvent.invitor_id = parseInt(user_id)
groupInviteEvent.flag = notify.seq
postOb11Event(groupInviteEvent)
}
@@ -413,6 +441,11 @@ function onLoad() {
async function start() {
log('llonebot pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB){
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
llonebotError.otherError = ''
startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => {
@@ -420,9 +453,33 @@ function onLoad() {
uidMaps[value] = key
}
})
startReceiveHook().then()
NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
try{
log('start get groups')
const _groups = await NTQQGroupApi.getGroups()
log('_groups', _groups)
await Promise.all(
_groups.map(async (group) => {
try {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = members
groups.push(group)
} catch (e) {
log('获取群成员失败', e)
}
})
)
}
catch (e) {
log('获取群列表失败', e)
}
finally {
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
}
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}

View File

@@ -10,15 +10,23 @@ import {
ElementType,
IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement,
RawMessage,
} from '../types'
import path from 'path'
import fs from 'fs'
import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import { Peer } from '@/ntqqapi/api/msg'
export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
return (await wrapperApi.NodeIQQNTWrapperSession.getRichMediaService().getVideoPlayUrlV2(peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url;
}
static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API,

View File

@@ -7,15 +7,48 @@ import { log } from '../../common/utils/log'
import { NTQQWindowApi, NTQQWindows } from './window'
export class NTQQGroupApi {
static async activateMemberListChange(){
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async activateMemberInfoChange(){
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async getGroupAllInfo(groupCode: string, source: number=4){
return await callNTQQApi<GeneralCallResult & Group>({
methodName: NTQQApiMethod.GET_GROUP_ALL_INFO,
args: [
{
groupCode,
source
},
null,
],
})
}
static async getGroups(forced = false) {
let cbCmd = ReceiveCmdS.GROUPS
if (process.platform != 'win32') {
cbCmd = ReceiveCmdS.GROUPS_STORE
}
// let cbCmd = ReceiveCmdS.GROUPS
// if (process.platform != 'win32') {
// cbCmd = ReceiveCmdS.GROUPS_STORE
// }
const result = await callNTQQApi<{
updateType: number
groupList: Group[]
}>({ methodName: NTQQApiMethod.GROUPS, args: [{ force_update: forced }, undefined], cbCmd })
}>({
methodName: NTQQApiMethod.GROUPS,
args: [{ force_update: forced }, undefined],
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE],
afterFirstCmd: false,
})
log('get groups result', result)
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
@@ -58,6 +91,19 @@ export class NTQQGroupApi {
return []
}
}
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean=false) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GROUP_MEMBERS_INFO,
args: [
{
forceUpdate,
groupCode,
uids
},
null,
],
})
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
@@ -158,7 +204,8 @@ export class NTQQGroupApi {
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
NTQQGroupApi.activateMemberListChange().then().catch(log)
const res = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
@@ -169,6 +216,8 @@ export class NTQQGroupApi {
null,
],
})
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res;
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({

View File

@@ -7,7 +7,9 @@ import { log } from '../../common/utils/log'
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: callbackFunc
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage
export interface Peer {
chatType: ChatType
@@ -29,7 +31,8 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
await sleep(500)
checkLastSendUsingTime += 500
return await waitLastSend()
} else {
}
else {
return
}
}
@@ -39,16 +42,20 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
sentMessages[rawMessage.msgId] = rawMessage
}
let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) {
if (sentMessage.sendStatus == 2) {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
} else {
}
else {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
@@ -60,10 +67,28 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
await sleep(500)
return await checkSendComplete()
}
return checkSendComplete();
return checkSendComplete()
}
export class NTQQMsgApi {
static enterOrExitAIO(peer: Peer, enter: boolean) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO,
args: [
{
"info_list": [
{
peer,
"option": enter ? 1 : 2
}
]
},
{
"send": true
},
],
})
}
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
@@ -83,6 +108,7 @@ export class NTQQMsgApi {
],
})
}
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTQQApiMethod.GET_MULTI_MSG,
@@ -97,6 +123,20 @@ export class NTQQMsgApi {
})
}
static async getMsgBoxInfo(peer: Peer) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GET_MSG_BOX_INFO,
args: [
{
contacts: [
peer
],
},
null,
],
})
}
static async activateChat(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
@@ -105,6 +145,7 @@ export class NTQQMsgApi {
args: [{ peer, cnt: 20 }, null],
})
}
static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
@@ -114,6 +155,7 @@ export class NTQQMsgApi {
args: [{ peer, cnt: 20 }, null],
})
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
@@ -129,6 +171,7 @@ export class NTQQMsgApi {
],
})
}
static async fetchRecentContact() {
await callNTQQApi({
methodName: NTQQApiMethod.RECENT_CONTACT,
@@ -164,7 +207,7 @@ export class NTQQMsgApi {
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
const waiter = sendWaiter(peer, waitComplete, timeout);
const waiter = sendWaiter(peer, waitComplete, timeout)
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [
@@ -177,11 +220,11 @@ export class NTQQMsgApi {
null,
],
}).then()
return await waiter;
return await waiter
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const waiter = sendWaiter(destPeer, true, 10000);
const waiter = sendWaiter(destPeer, true, 10000)
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
@@ -195,7 +238,7 @@ export class NTQQMsgApi {
null,
],
}).then().catch(log)
return await waiter;
return await waiter
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {

View File

@@ -6,6 +6,7 @@ import { NTQQWindowApi, NTQQWindows } from './window'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import * as https from 'https'
import { RequestUtil } from '@/common/utils/request'
let userInfoCache: Record<string, User> = {} // uid: User
@@ -47,9 +48,12 @@ export class NTQQUserApi {
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string, getLevel = false) {
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
// this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
if (!withBizInfo) {
methodName = NTQQApiMethod.USER_DETAIL_INFO
}
const fetchInfo = async () => {
const result = await callNTQQApi<{ info: User }>({
methodName,
@@ -95,7 +99,17 @@ export class NTQQUserApi {
],
})
}
static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string; } = {};
try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl);
} catch (e: any) {
log('获取QZone Cookies失败', e)
cookies = {}
}
return cookies;
}
static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey()
if (clientKeyData.result !== 0) {
@@ -103,33 +117,19 @@ export class NTQQUserApi {
}
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return new Promise((resolve, reject) => {
const req = https.get(url, (res) => {
const rawCookies = res.headers['set-cookie']
const cookies = {}
rawCookies.forEach(cookie => {
// 使用正则表达式匹配 cookie 名称和值
const regex = /([^=;]+)=([^;]*)/
const match = regex.exec(cookie)
if (match) {
cookies[match[1].trim()] = match[2].trim()
}
})
resolve(cookies['skey'])
})
req.on('error', e => {
reject(e)
});
req.end();
});
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex;
return (await RequestUtil.HttpsGetCookies(url))?.skey;
}
@cacheFunc(60 * 30 * 1000)
static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) {
let data = (await NTQQUserApi.getQzoneCookies());
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin;
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue };
}
const skey = await this.getSkey();
const pskey= (await this.getPSkey([domain])).get(domain);
const pskey = (await this.getPSkey([domain])).get(domain);
if (!pskey || !skey) {
throw new Error('获取Cookies失败')
}

View File

@@ -22,6 +22,7 @@ import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils'
import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -324,207 +325,222 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
}
}
// 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
} else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
} else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
}
})
export async function startHook() {
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) {
Object.assign(existMember, member)
// 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
}
// const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) {
if (member.cardName != existMember.cardName) {
log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
postOb11Event(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
)
}
Object.assign(existMember, member)
}
}
// const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
// 好友列表变动
registerReceiveHook<{
data:CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0;
rawFriends.push(...payload.data);
for (const fData of payload.data) {
const _friends = fData.buddyList
for (let friend of _friends) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
} else {
Object.assign(existFriend, friend)
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0;
rawFriends.push(...payload.data);
for (const fData of payload.data) {
const _friends = fData.buddyList
for (let friend of _friends) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
}
}
}
}
})
})
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
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord
const peerUid = message.peerUid
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
} catch (e) {
log('receive self msg error', e.stack)
}
}
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20
})
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id)
const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) {
log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg.msgTime) < 5) {
OB11Constructor.message(lastTempMsg).then((r) => postOb11Event(r))
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)
}
})
})
} else {
NTQQMsgApi.activateChat(peer).then()
}
uidMaps[uid] = uin
}
}
}
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
} else {
// 检查是否好友
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
// 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
}
}
}
const peer = { peerUid, chatType }
await sleep(1000)
NTQQMsgApi.activateChat(peer).then((r) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord
const peerUid = message.peerUid
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
} catch (e) {
log('receive self msg error', e.stack)
}
}
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20
})
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id)
const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) {
log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg.msgTime) < 5) {
OB11Constructor.message(lastTempMsg).then((r) => postOb11Event(r))
}
})
})
}
else {
NTQQMsgApi.activateChat(peer).then()
}
}
}
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else {
// 检查是否好友
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
}
}
const peer = { peerUid, chatType }
await sleep(1000)
NTQQMsgApi.activateChat(peer).then((r) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
}

View File

@@ -27,13 +27,17 @@ export enum NTQQApiMethod {
HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio',
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = 'fetchAuthData',
FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = 'nodeIKernelGroupService/getGroupList',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
@@ -65,6 +69,10 @@ export enum NTQQApiMethod {
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle',
ACTIVATE_MEMBER_LIST_CHANGE = 'nodeIKernelGroupListener/onMemberListChange',
ACTIVATE_MEMBER_INFO_CHANGE = 'nodeIKernelGroupListener/onMemberInfoChange',
GET_MSG_BOX_INFO = 'nodeIKernelMsgService/getABatchOfContactMsgBoxInfo',
GET_GROUP_ALL_INFO = 'nodeIKernelGroupService/getGroupAllInfo',
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
@@ -99,7 +107,7 @@ interface NTQQApiParams {
channel?: NTQQApiChannel
classNameIsRegister?: boolean
args?: unknown[]
cbCmd?: ReceiveCmd | null
cbCmd?: ReceiveCmd | ReceiveCmd[] | null
cmdCB?: (payload: any) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number
@@ -139,7 +147,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true
resolve(r)
}
} else {
}
else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
@@ -150,7 +159,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true
resolve(payload)
}
} else {
}
else {
removeReceiveHook(hookId)
success = true
resolve(payload)
@@ -162,7 +172,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback()
} else {
}
else {
success = true
reject(`ntqq api call failed, ${result.errMsg}`)
}
@@ -180,7 +191,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
channel,
{
sender: {
send: (..._args: unknown[]) => {},
send: (..._args: unknown[]) => {
},
},
},
{ type: 'request', callbackId: uuid, eventName },

View File

@@ -188,6 +188,7 @@ export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn'
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export interface PicElement {
picSubType: PicSubType
picType: PicType // 有这玩意儿吗
originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string
@@ -226,6 +227,7 @@ export interface GrayTipElement {
content: string
}
jsonGrayTipElement: {
busiId: number
jsonStr: string
}
}

View File

@@ -1,6 +1,7 @@
export enum GroupNotifyTypes {
INVITE_ME = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST_BY_INVITED = 5, // 有人邀请了别人入群
JOIN_REQUEST = 7,
ADMIN_SET = 8,
KICK_MEMBER = 9,

View File

@@ -2,7 +2,7 @@ import BaseAction from '../BaseAction'
import fs from 'fs/promises'
import { dbUtil } from '@/common/db'
import { getConfigUtil } from '@/common/config'
import { log, sleep, uri2local } from '@/common/utils'
import { checkFileReceived, log, sleep, uri2local } from '@/common/utils'
import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types'
@@ -38,20 +38,21 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
log('找到了文件 element', element)
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true)
await sleep(1000) // 这里延时是为何?
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}
}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file)
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
let cache = await dbUtil.getFileCache(payload.file)
if (!cache) {
throw new Error('file not found')
}
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (cache.downloadFunc) {
await cache.downloadFunc()
}

View File

@@ -1,5 +1,9 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types'
import {decodeSilk} from "@/common/utils/audio";
import { getConfigUtil } from '@/common/config'
import path from 'node:path'
import fs from 'node:fs'
interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
@@ -9,7 +13,13 @@ export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = super._handle(payload)
let res = await super._handle(payload)
res.file = await decodeSilk(res.file, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){
res.base64 = fs.readFileSync(res.file, 'base64')
}
return res
}
}

View File

@@ -46,7 +46,7 @@ import GetFile from './file/GetFile'
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardSingleGroupMsg } from './msg/ForwardSingleMsg'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
@@ -91,7 +91,7 @@ export const actionHandlers = [
new GetCookies(),
new SetMsgEmojiLike(),
new ForwardFriendSingleMsg(),
new ForwardSingleGroupMsg(),
new ForwardGroupSingleMsg(),
//以下为go-cqhttp api
new GetGroupEssence(),
new GetGroupHonorInfo(),

View File

@@ -43,6 +43,6 @@ export class ForwardFriendSingleMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardFriendSingleMsg
}
export class ForwardSingleGroupMsg extends ForwardSingleMsg {
export class ForwardGroupSingleMsg extends ForwardSingleMsg {
actionName = ActionName.ForwardGroupSingleMsg
}

View File

@@ -327,7 +327,7 @@ export async function sendMsg(
}
}
log('发送消息总大小', totalSize, 'bytes')
let timeout = ((totalSize / 1024 / 512) * 1000) + 5000 // 512kb/s
let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s
log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
log('消息发送结果', returnMsg)

View File

@@ -10,6 +10,7 @@ import { ChatType, Group, GroupRequestOperateTypes } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils'
import { getConfigUtil } from '@/common/config'
interface QuickOperationPrivateMessage {
@@ -49,8 +50,8 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc
handleMsg(context as OB11Message, quickAction).then().catch(log)
}
if (context.post_type === 'request') {
const friendRequest = context as OB11FriendRequestEvent;
const groupRequest = context as OB11GroupRequestEvent;
const friendRequest = context as OB11FriendRequestEvent
const groupRequest = context as OB11GroupRequestEvent
if ((friendRequest).request_type === 'friend') {
handleFriendRequest(friendRequest, quickAction).then().catch(log)
}
@@ -64,6 +65,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
msg = msg as OB11Message
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11
let peer: Peer = {
chatType: ChatType.friend,
peerUid: msg.user_id.toString(),
@@ -80,14 +82,15 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
}
if (reply) {
let group: Group = null
let replyMessage: OB11MessageData[] = [
{
let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) {
replyMessage.push({
type: OB11MessageDataType.reply,
data: {
id: msg.message_id.toString(),
},
}
]
})
}
if (msg.message_type == 'group') {
group = await getGroup(msg.group_id.toString())

View File

@@ -1,17 +1,16 @@
import BaseAction from '../BaseAction'
import { NTQQUserApi } from '../../../ntqqapi/api'
import { groups } from '../../../common/data'
import { ActionName } from '../types'
interface Payload {
domain: string
}
export class GetCookies extends BaseAction<Payload, { cookies: string; bkn: string }> {
actionName = ActionName.GetCookies
protected async _handle(payload: Payload) {
const domain = payload.domain || 'qun.qq.com'
return NTQQUserApi.getCookies(domain);
}
}
import BaseAction from '../BaseAction'
import { NTQQUserApi } from '@/ntqqapi/api'
import { ActionName } from '../types'
interface Payload {
domain: string
}
export class GetCookies extends BaseAction<Payload, { cookies: string; bkn: string }> {
actionName = ActionName.GetCookies
protected async _handle(payload: Payload) {
const domain = payload.domain || 'qun.qq.com'
return NTQQUserApi.getCookies(domain);
}
}

View File

@@ -24,7 +24,7 @@ import {
User,
VideoElement,
} from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap } from '../common/data'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data'
import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db'
@@ -47,6 +47,8 @@ import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent';
let lastRKeyUpdateTime = 0
@@ -161,10 +163,12 @@ export class OB11Constructor {
// message_data["data"]["file"] = element.picElement.sourcePath
let fileName = element.picElement.fileName
const sourcePath = element.picElement.sourcePath
if (element.picElement.picType === PicType.gif && !fileName.endsWith('.gif')) {
const isGif = element.picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif'
}
message_data['data']['file'] = fileName
message_data['data']['subType'] = element.picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
@@ -188,9 +192,7 @@ export class OB11Constructor {
element.picElement.sourcePath,
)
},
})
.then()
// 不在自动下载图片
}).then()
}
else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement
@@ -200,6 +202,13 @@ export class OB11Constructor {
message_data['data']['path'] = videoOrFileElement.filePath
message_data['data']['file_id'] = videoOrFileElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize
if (element.videoElement) {
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId, element.elementId,
)
}
dbUtil
.addFileCache(videoOrFileElement.fileUuid, {
msgId: msg.msgId,
@@ -290,7 +299,7 @@ export class OB11Constructor {
if (message_data.type !== 'unknown' && message_data.data) {
const cqCode = encodeCQCode(message_data)
if (messagePostFormat === 'string') {
;(resMsg.message as string) += cqCode
(resMsg.message as string) += cqCode
}
else (resMsg.message as OB11MessageData[]).push(message_data)
@@ -301,6 +310,35 @@ export class OB11Constructor {
return resMsg
}
static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent> {
if (msg.chatType !== ChatType.friend) {
return;
}
for (const element of msg.elements) {
if (element.grayTipElement) {
if (element.grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr);
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
let pokedetail: any[] = json.items;
//筛选item带有uid的元素
pokedetail = pokedetail.filter(item => item.uid);
if (pokedetail.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[pokedetail[0].uid])!), parseInt((uidMaps[pokedetail[1].uid])));
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
}
}
}
// 好友增加事件
if (msg.msgType === 5 && msg.subMsgType === 12) {
const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin))
return event
}
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent> {
if (msg.chatType !== ChatType.group) {
return
@@ -485,6 +523,16 @@ export class OB11Constructor {
}
* */
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
let pokedetail: any[] = json.items;
//筛选item带有uid的元素
pokedetail = pokedetail.filter(item => item.uid);
if (pokedetail.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[pokedetail[0].uid])!), parseInt((uidMaps[pokedetail[1].uid])));
}
}
const memberUin = json.items[1].param[0]
const title = json.items[3].txt
log('收到群成员新头衔消息', json)
@@ -497,17 +545,6 @@ export class OB11Constructor {
}
}
static async FriendAddEvent(msg: RawMessage): Promise<OB11FriendAddNoticeEvent | undefined> {
if (msg.chatType !== ChatType.friend) {
return
}
if (msg.msgType === 5 && msg.subMsgType === 12) {
const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin))
return event
}
return
}
static async RecallEvent(
msg: RawMessage,
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {

View File

@@ -50,6 +50,7 @@ export function encodeCQCode(data: OB11MessageData) {
}
const CQCodeEscape = (text: string) => {
text = text.toString()
return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;')
}

View File

@@ -10,21 +10,20 @@ class OB11PokeEvent extends OB11BaseNoticeEvent {
}
export class OB11FriendPokeEvent extends OB11PokeEvent {
sender_id: number
constructor(user_id: number) {
super()
this.user_id = user_id
this.sender_id = user_id
constructor(user_id: number, target_id: number) {
super();
this.target_id = target_id;
this.user_id = user_id;
}
}
export class OB11GroupPokeEvent extends OB11PokeEvent {
group_id: number
constructor(group_id: number, user_id: number = 0) {
constructor(group_id: number, user_id: number = 0, target_id: number = 0) {
super()
this.group_id = group_id
this.target_id = user_id
this.target_id = target_id
this.user_id = user_id
}
}

View File

@@ -5,6 +5,7 @@ export class OB11GroupRequestEvent extends OB11GroupNoticeEvent {
post_type = EventType.REQUEST
request_type: 'group' = 'group'
sub_type: 'add' | 'invite' = 'add'
invitor_id: number | undefined = undefined
comment: string
flag: string
}

View File

@@ -10,6 +10,7 @@ import { WebSocket as WebSocketClass } from 'ws'
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent'
import { log } from '../../../common/utils/log'
import { getConfigUtil } from '../../../common/config'
import { version } from '../../../version'
export let rwsList: ReverseWebsocket[] = []
@@ -85,6 +86,7 @@ export class ReverseWebsocket {
'X-Self-ID': selfInfo.uin,
Authorization: `Bearer ${token}`,
'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段
'User-Agent': `LLOneBot/${version}`,
},
})
registerWsEventSender(this.websocket)

View File

@@ -27,6 +27,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
}
try {
let handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo
wsReply(wsClient, handleResult)
} catch (e) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))

View File

@@ -62,6 +62,13 @@ async function onSettingWindowCreated(view: Element) {
SettingButton('请稍候', 'llonebot-update-button', 'secondary'),
),
]),
SettingList([
SettingItem(
'是否启用 LLOneBot, 重启QQ后生效',
null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
)]
),
SettingList([
SettingItem(
'启用 HTTP 服务',
@@ -171,6 +178,11 @@ async function onSettingWindowCreated(view: Element) {
`<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="musicSignUrl" type="text" value="${config.musicSignUrl}" placeholder="未设置" /></div>`,
'config-musicSignUrl',
),
SettingItem(
'快速操作回复自动引用原消息',
null,
SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote, { 'control-display-id': 'config-ob11-enableQOAutoQuote' }),
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')),
]),
SettingList([

View File

@@ -1 +1 @@
export const version = '3.26.1'
export const version = '3.27.2'