Compare commits

...

52 Commits

Author SHA1 Message Date
linyuchen
d0967785de chore: v3.27.3 2024-07-20 16:58:03 +08:00
linyuchen
eccabb8189 Merge pull request #299 from Natsukage/main
fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
2024-07-20 15:25:27 +08:00
夏影
c9374ff515 fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
Added logic to skip name-value pairs in encodeCQCode when value cannot be converted to string, preventing errors caused by undefined values. This ensures the function can handle such cases gracefully and continue processing other valid data.
2024-07-20 00:49:34 +08:00
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 705 additions and 371 deletions

View File

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

View File

@@ -23,7 +23,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.3.4", "silk-wasm": "^3.6.0",
"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

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

View File

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

View File

@@ -3,26 +3,34 @@ import fs from 'node:fs'
import os from 'node:os' import os from 'node:os'
import { systemPlatform } from './system' 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'); function getPKGPath() {
let configVersionInfoPath; 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') { if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json'); configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json')
} else { }
const userPath = os.homedir(); else {
const appDataPath = path.resolve(userPath, './.config/QQ'); const userPath = os.homedir()
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json'); const appDataPath = path.resolve(userPath, './.config/QQ')
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json')
} }
if (typeof configVersionInfoPath !== 'string') { 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 = { type QQPkgInfo = {
version: string; version: string;
@@ -43,21 +51,21 @@ let _qqVersionConfigInfo: QQVersionConfigInfo = {
'curVersion': '9.9.9-23361', 'curVersion': '9.9.9-23361',
'prevVersion': '', 'prevVersion': '',
'onErrorVersions': [], 'onErrorVersions': [],
'buildId': '23361' 'buildId': '23361',
}; }
if (fs.existsSync(configVersionInfoPath)) { if (fs.existsSync(configVersionInfoPath)) {
try { try {
const _ =JSON.parse(fs.readFileSync(configVersionInfoPath).toString()); const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString())
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _); _qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _)
} catch (e) { } 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, // platform_type: 3,
// app_type: 4, // app_type: 4,
// app_version: '9.9.9-23159', // app_version: '9.9.9-23159',
@@ -66,10 +74,10 @@ export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath);
// platVer: '10.0.26100', // platVer: '10.0.26100',
// clientVer: '9.9.9-23159', // clientVer: '9.9.9-23159',
let _appid: string = '537213803'; // 默认为 Windows 平台的 appid let _appid: string = '537213803' // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') { if (systemPlatform === 'linux') {
_appid = '537213827'; _appid = '537213827'
} }
// todo: mac 平台的 appid // todo: mac 平台的 appid
export const appid = _appid; export const appid = _appid
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106' export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

View File

@@ -1,9 +1,9 @@
import fs from 'fs' import fs from 'fs'
import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm'
import fsPromise from 'fs/promises' import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log' import { log } from './log'
import path from 'node:path' import path from 'node:path'
import { DATA_DIR, TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
@@ -60,10 +60,11 @@ export async function encodeSilk(filePath: string) {
// } // }
try { try {
const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, uuidv4()) const pttPath = path.join(TEMP_DIR, uuidv4())
if (getFileHeader(filePath) !== '02232153494c4b') { if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath) const _isWav = isWav(file)
const pcmPath = pttPath + '.pcm' const pcmPath = pttPath + '.pcm'
let sampleRate = 0 let sampleRate = 0
const convert = () => { const convert = () => {
@@ -79,7 +80,8 @@ export async function encodeSilk(filePath: string) {
if (code == null || EXIT_CODES.includes(code)) { if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000 sampleRate = 24000
const data = fs.readFileSync(pcmPath) const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {}) fs.unlink(pcmPath, (err) => {
})
return resolve(data) return resolve(data)
} }
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`) log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
@@ -91,7 +93,7 @@ export async function encodeSilk(filePath: string) {
if (!_isWav) { if (!_isWav) {
input = await convert() input = await convert()
} else { } else {
input = fs.readFileSync(filePath) input = file
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input) const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt) // log(`wav文件信息`, fmt)
@@ -108,7 +110,7 @@ export async function encodeSilk(filePath: string) {
duration: silk.duration / 1000, duration: silk.duration / 1000,
} }
} else { } else {
const silk = fs.readFileSync(filePath) const silk = file
let duration = 0 let duration = 0
try { try {
duration = getDuration(silk) / 1000 duration = getDuration(silk) / 1000
@@ -128,3 +130,41 @@ export async function encodeSilk(filePath: string) {
return {} 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 https from 'node:https';
import http from 'node:http'; import http from 'node:http';
import { log } from '@/common/utils/log'
export class RequestUtil { export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET // 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<Map<string, string>> { static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
return new Promise<Map<string, string>>((resolve, reject) => { const client = url.startsWith('https') ? https : http;
const protocol = url.startsWith('https://') ? https : http; return new Promise((resolve, reject) => {
protocol.get(url, (res) => { client.get(url, (res) => {
const cookiesHeader = res.headers['set-cookie']; let cookies: { [key: string]: string } = {};
if (!cookiesHeader) { const handleRedirect = (res: http.IncomingMessage) => {
resolve(new Map<string, string>()); //console.log(res.headers.location);
} else { if (res.statusCode === 301 || res.statusCode === 302) {
const cookiesMap = new Map<string, string>(); if (res.headers.location) {
cookiesHeader.forEach((cookieStr) => { const redirectUrl = new URL(res.headers.location, url);
cookieStr.split(';').forEach((cookiePart) => { RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
const trimmedPart = cookiePart.trim(); // 合并重定向过程中的cookies
if (trimmedPart.includes('=')) { log('redirectCookies', redirectCookies)
const [key, value] = trimmedPart.split('=').map(part => part.trim()); cookies = { ...cookies, ...redirectCookies };
cookiesMap.set(key, decodeURIComponent(value)); // 解码cookie值 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) => { }).on('error', (err) => {
reject(error); reject(err);
}); });
}); });
} }

View File

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

View File

@@ -10,15 +10,23 @@ import {
ElementType, ElementType,
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement, IMAGE_HTTP_HOST_NT, PicElement,
RawMessage,
} from '../types' } from '../types'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey' import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import { Peer } from '@/ntqqapi/api/msg'
export class NTQQFileApi { 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) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,

View File

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

View File

@@ -7,7 +7,9 @@ import { log } from '../../common/utils/log'
import { sleep } from '../../common/utils/helper' import { sleep } from '../../common/utils/helper'
import { isQQ998 } from '../../common/utils' 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 { export interface Peer {
chatType: ChatType chatType: ChatType
@@ -29,7 +31,8 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
await sleep(500) await sleep(500)
checkLastSendUsingTime += 500 checkLastSendUsingTime += 500
return await waitLastSend() return await waitLastSend()
} else { }
else {
return return
} }
} }
@@ -39,16 +42,20 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid] delete sendMessagePool[peerUid]
sentMessage = rawMessage sentMessage = rawMessage
sentMessages[rawMessage.msgId] = rawMessage
} }
let checkSendCompleteUsingTime = 0 let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => { const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) { if (sentMessage) {
if (waitComplete) { if (waitComplete) {
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) { if (sentMessage.sendStatus == 2) {
delete sentMessages[sentMessage.msgId]
return sentMessage return sentMessage
} }
} else { }
else {
delete sentMessages[sentMessage.msgId]
return sentMessage return sentMessage
} }
// log(`给${peerUid}发送消息成功`) // log(`给${peerUid}发送消息成功`)
@@ -60,10 +67,28 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
await sleep(500) await sleep(500)
return await checkSendComplete() return await checkSendComplete()
} }
return checkSendComplete(); return checkSendComplete()
} }
export class NTQQMsgApi { 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) { 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//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // 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) { static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({ return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTQQApiMethod.GET_MULTI_MSG, 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) { static async activateChat(peer: Peer) {
// await this.fetchRecentContact(); // await this.fetchRecentContact();
// await sleep(500); // await sleep(500);
@@ -105,6 +145,7 @@ export class NTQQMsgApi {
args: [{ peer, cnt: 20 }, null], args: [{ peer, cnt: 20 }, null],
}) })
} }
static async activateChatAndGetHistory(peer: Peer) { static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact(); // await this.fetchRecentContact();
// await sleep(500); // await sleep(500);
@@ -114,6 +155,7 @@ export class NTQQMsgApi {
args: [{ peer, cnt: 20 }, null], args: [{ peer, cnt: 20 }, null],
}) })
} }
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[] }>({
@@ -129,6 +171,7 @@ export class NTQQMsgApi {
], ],
}) })
} }
static async fetchRecentContact() { static async fetchRecentContact() {
await callNTQQApi({ await callNTQQApi({
methodName: NTQQApiMethod.RECENT_CONTACT, methodName: NTQQApiMethod.RECENT_CONTACT,
@@ -164,7 +207,7 @@ export class NTQQMsgApi {
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
const waiter = sendWaiter(peer, waitComplete, timeout); const waiter = sendWaiter(peer, waitComplete, timeout)
callNTQQApi({ callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG, methodName: NTQQApiMethod.SEND_MSG,
args: [ args: [
@@ -177,11 +220,11 @@ export class NTQQMsgApi {
null, null,
], ],
}).then() }).then()
return await waiter; return await waiter
} }
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const waiter = sendWaiter(destPeer, true, 10000); const waiter = sendWaiter(destPeer, true, 10000)
callNTQQApi<GeneralCallResult>({ callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG, methodName: NTQQApiMethod.FORWARD_MSG,
args: [ args: [
@@ -195,7 +238,7 @@ export class NTQQMsgApi {
null, null,
], ],
}).then().catch(log) }).then().catch(log)
return await waiter; return await waiter
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { 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 { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper' import { wrapperApi } from '@/ntqqapi/native/wrapper'
import * as https from 'https' import * as https from 'https'
import { RequestUtil } from '@/common/utils/request'
let userInfoCache: Record<string, User> = {} // uid: User let userInfoCache: Record<string, User> = {} // uid: User
@@ -47,9 +48,12 @@ export class NTQQUserApi {
return result.profiles.get(uid) return result.profiles.get(uid)
} }
static async getUserDetailInfo(uid: string, getLevel = false) { static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
// this.getUserInfo(uid); // this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO 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 fetchInfo = async () => {
const result = await callNTQQApi<{ info: User }>({ const result = await callNTQQApi<{ info: User }>({
methodName, 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> { static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey() const clientKeyData = await this.getClientKey()
if (clientKeyData.result !== 0) { if (clientKeyData.result !== 0) {
@@ -103,33 +117,19 @@ export class NTQQUserApi {
} }
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey + '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex + '&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;
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();
});
} }
@cacheFunc(60 * 30 * 1000) @cacheFunc(60 * 30 * 1000)
static async getCookies(domain: string) { 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 skey = await this.getSkey();
const pskey= (await this.getPSkey([domain])).get(domain); const pskey = (await this.getPSkey([domain])).get(domain);
if (!pskey || !skey) { if (!pskey || !skey) {
throw new Error('获取Cookies失败') throw new Error('获取Cookies失败')
} }

View File

@@ -22,6 +22,7 @@ import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { isNumeric, sleep } from '@/common/utils' import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -324,207 +325,222 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
} }
} }
// 群列表变动 export async function startHook() {
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()
}
}
})
registerReceiveHook<{ // 群列表变动
groupCode: string registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
dataSource: number // updateType 3是群列表变动2是群成员变动
members: Set<GroupMember> // log("群列表变动", payload.updateType, payload.groupList)
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => { if (payload.updateType != 2) {
const groupCode = payload.groupCode updateGroups(payload.groupList).then()
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)
} }
} else {
// const existGroup = groups.find(g => g.groupCode == groupCode); if (process.platform == 'win32') {
// if (existGroup) { processGroupEvent(payload).then()
// log("对比群成员", existGroup.members, members) }
// for (const member of members) { }
// const existMember = existGroup.members.find(m => m.uin == member.uin); })
// if (existMember) { registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// log("对比群名片", existMember.cardName, member.cardName) // updateType 3是群列表变动2是群成员变动
// if (existMember.cardName != member.cardName) { // log("群列表变动", payload.updateType, payload.groupList)
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName)); if (payload.updateType != 2) {
// } updateGroups(payload.groupList).then()
// Object.assign(existMember, member); }
// } 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<{ registerReceiveHook<{
data:CategoryFriend[] data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => { }>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0; rawFriends.length = 0;
rawFriends.push(...payload.data); rawFriends.push(...payload.data);
for (const fData of payload.data) { for (const fData of payload.data) {
const _friends = fData.buddyList const _friends = fData.buddyList
for (let friend of _friends) { for (let friend of _friends) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin) let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) { if (!existFriend) {
friends.push(friend) friends.push(friend)
} else { }
Object.assign(existFriend, friend) else {
Object.assign(existFriend, friend)
}
} }
} }
} })
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid // 保存一下uid
for (const message of payload.msgList) { for (const message of payload.msgList) {
const uid = message.senderUid const uid = message.senderUid
const uin = message.senderUin const uin = message.senderUin
if (uid && uin) { if (uid && uin) {
if (message.chatType === ChatType.temp) { if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => { dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) { if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap) 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))
} }
}) })
}) }
} else { uidMaps[uid] = uin
NTQQMsgApi.activateChat(peer).then()
} }
} }
}
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => { // 自动清理新消息文件
const peerUid = payload[0] as string const { autoDeleteFile } = getConfigUtil().getConfig()
log('激活的聊天窗口被删除,准备重新激活', peerUid) if (!autoDeleteFile) {
let chatType = ChatType.friend return
if (isNumeric(peerUid)) { }
chatType = ChatType.group for (const message of payload.msgList) {
} else { // log("收到新消息push到历史记录", message.msgId)
// 检查是否好友 // dbUtil.addMsg(message).then()
if (!(await getFriend(peerUid))) { // 清理文件
chatType = ChatType.temp
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', HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg', GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid', DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio',
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike', LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = 'fetchAuthData', SELF_INFO = 'fetchAuthData',
FRIENDS = 'nodeIKernelBuddyService/getBuddyList', FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = 'nodeIKernelGroupService/getGroupList', GROUPS = 'nodeIKernelGroupService/getGroupList',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene', GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList', GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo', USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo', USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo', USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
@@ -65,6 +69,10 @@ export enum NTQQApiMethod {
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin', PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName', SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle', 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_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths', CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
@@ -99,7 +107,7 @@ interface NTQQApiParams {
channel?: NTQQApiChannel channel?: NTQQApiChannel
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[] args?: unknown[]
cbCmd?: ReceiveCmd | null cbCmd?: ReceiveCmd | ReceiveCmd[] | null
cmdCB?: (payload: any) => boolean cmdCB?: (payload: any) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number timeoutSecond?: number
@@ -139,7 +147,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true success = true
resolve(r) resolve(r)
} }
} else { }
else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
@@ -150,7 +159,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
success = true success = true
resolve(payload) resolve(payload)
} }
} else { }
else {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true success = true
resolve(payload) resolve(payload)
@@ -162,7 +172,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
log(`${methodName} callback`, result) log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) { if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} else { }
else {
success = true success = true
reject(`ntqq api call failed, ${result.errMsg}`) reject(`ntqq api call failed, ${result.errMsg}`)
} }
@@ -180,7 +191,8 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
channel, channel,
{ {
sender: { sender: {
send: (..._args: unknown[]) => {}, send: (..._args: unknown[]) => {
},
}, },
}, },
{ type: 'request', callbackId: uuid, eventName }, { 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 const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export interface PicElement { export interface PicElement {
picSubType: PicSubType
picType: PicType // 有这玩意儿吗 picType: PicType // 有这玩意儿吗
originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string originImageMd5?: string
@@ -226,6 +227,7 @@ export interface GrayTipElement {
content: string content: string
} }
jsonGrayTipElement: { jsonGrayTipElement: {
busiId: number
jsonStr: string jsonStr: string
} }
} }

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile' import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types' 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 { interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
@@ -9,7 +13,13 @@ export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> { 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 return res
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ import {
User, User,
VideoElement, VideoElement,
} from '../ntqqapi/types' } 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 { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db' import { dbUtil } from '../common/db'
@@ -47,6 +47,8 @@ import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent' import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent' import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent';
let lastRKeyUpdateTime = 0 let lastRKeyUpdateTime = 0
@@ -161,10 +163,12 @@ export class OB11Constructor {
// message_data["data"]["file"] = element.picElement.sourcePath // message_data["data"]["file"] = element.picElement.sourcePath
let fileName = element.picElement.fileName let fileName = element.picElement.fileName
const sourcePath = element.picElement.sourcePath 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' fileName += '.gif'
} }
message_data['data']['file'] = fileName message_data['data']['file'] = fileName
message_data['data']['subType'] = element.picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
@@ -188,9 +192,7 @@ export class OB11Constructor {
element.picElement.sourcePath, element.picElement.sourcePath,
) )
}, },
}) }).then()
.then()
// 不在自动下载图片
} }
else if (element.videoElement || element.fileElement) { else if (element.videoElement || element.fileElement) {
const videoOrFileElement = 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']['path'] = videoOrFileElement.filePath
message_data['data']['file_id'] = videoOrFileElement.fileUuid message_data['data']['file_id'] = videoOrFileElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize 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 dbUtil
.addFileCache(videoOrFileElement.fileUuid, { .addFileCache(videoOrFileElement.fileUuid, {
msgId: msg.msgId, msgId: msg.msgId,
@@ -290,7 +299,7 @@ export class OB11Constructor {
if (message_data.type !== 'unknown' && message_data.data) { if (message_data.type !== 'unknown' && message_data.data) {
const cqCode = encodeCQCode(message_data) const cqCode = encodeCQCode(message_data)
if (messagePostFormat === 'string') { if (messagePostFormat === 'string') {
;(resMsg.message as string) += cqCode (resMsg.message as string) += cqCode
} }
else (resMsg.message as OB11MessageData[]).push(message_data) else (resMsg.message as OB11MessageData[]).push(message_data)
@@ -301,6 +310,35 @@ export class OB11Constructor {
return resMsg 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> { static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent> {
if (msg.chatType !== ChatType.group) { if (msg.chatType !== ChatType.group) {
return 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 memberUin = json.items[1].param[0]
const title = json.items[3].txt const title = json.items[3].txt
log('收到群成员新头衔消息', json) 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( static async RecallEvent(
msg: RawMessage, msg: RawMessage,
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> { ): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {

View File

@@ -50,6 +50,7 @@ export function encodeCQCode(data: OB11MessageData) {
} }
const CQCodeEscape = (text: string) => { const CQCodeEscape = (text: string) => {
text = text.toString()
return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;') return text.replace(/\&/g, '&amp;').replace(/\[/g, '&#91;').replace(/\]/g, '&#93;').replace(/,/g, '&#44;')
} }
@@ -60,6 +61,14 @@ export function encodeCQCode(data: OB11MessageData) {
let result = '[CQ:' + data.type let result = '[CQ:' + data.type
for (const name in data.data) { for (const name in data.data) {
const value = data.data[name] const value = data.data[name]
try {
// Check if the value can be converted to a string
value.toString();
} catch (error) {
// If it can't be converted, skip this name-value pair
// console.warn(`Skipping problematic name-value pair. Name: ${name}, Value: ${value}`);
continue;
}
result += `,${name}=${CQCodeEscape(value)}` result += `,${name}=${CQCodeEscape(value)}`
} }
result += ']' result += ']'

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
} }
try { try {
let handleResult = await action.websocketHandle(params, echo) let handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo
wsReply(wsClient, handleResult) wsReply(wsClient, handleResult)
} catch (e) { } catch (e) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) 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'), SettingButton('请稍候', 'llonebot-update-button', 'secondary'),
), ),
]), ]),
SettingList([
SettingItem(
'是否启用 LLOneBot, 重启QQ后生效',
null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
)]
),
SettingList([ SettingList([
SettingItem( SettingItem(
'启用 HTTP 服务', '启用 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>`, `<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', 'config-musicSignUrl',
), ),
SettingItem(
'快速操作回复自动引用原消息',
null,
SettingSwitch('ob11.enableQOAutoQuote', config.ob11.enableQOAutoQuote, { 'control-display-id': 'config-ob11-enableQOAutoQuote' }),
),
SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')), SettingItem('', null, SettingButton('保存', 'config-ob11-save', 'primary')),
]), ]),
SettingList([ SettingList([

View File

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