mirror of
https://github.com/LLOneBot/LLOneBot.git
synced 2024-11-22 01:56:33 +00:00
Compare commits
72 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
79c5041216 | ||
![]() |
8fb53260ab | ||
![]() |
07d9ac823a | ||
![]() |
296cd4d0a3 | ||
![]() |
e77a2ca34a | ||
![]() |
406e3c7e6b | ||
![]() |
3f5ca8ebfa | ||
![]() |
6e8389e833 | ||
![]() |
71aedca4c6 | ||
![]() |
6410689549 | ||
![]() |
6d0e2269cc | ||
![]() |
2e28fc678c | ||
![]() |
8204f4407f | ||
![]() |
9f1d4c4db2 | ||
![]() |
8ba47635d3 | ||
![]() |
5fa2427c51 | ||
![]() |
aa8739d016 | ||
![]() |
79f0329da7 | ||
![]() |
7a33a36f44 | ||
![]() |
808424d08e | ||
![]() |
d0967785de | ||
![]() |
eccabb8189 | ||
![]() |
c9374ff515 | ||
![]() |
92c4889924 | ||
![]() |
f9454039a1 | ||
![]() |
bc4511e175 | ||
![]() |
f191103f99 | ||
![]() |
408463f63b | ||
![]() |
fb96c4272e | ||
![]() |
c6b302d5a8 | ||
![]() |
1dd468e2ff | ||
![]() |
2a1aa8c649 | ||
![]() |
1633734e08 | ||
![]() |
dff92e6f27 | ||
![]() |
dba5e30d5d | ||
![]() |
2d04ab2e72 | ||
![]() |
1a015ac8d3 | ||
![]() |
6390620ddd | ||
![]() |
0d19005dc3 | ||
![]() |
c6479dd2c4 | ||
![]() |
8871331b7c | ||
![]() |
e01148b86a | ||
![]() |
2f87e3818e | ||
![]() |
2c8a594c38 | ||
![]() |
1508dab7fe | ||
![]() |
958b21e47e | ||
![]() |
781c3311ae | ||
![]() |
52850d172e | ||
![]() |
52a065542e | ||
![]() |
fd10469685 | ||
![]() |
a2ee75b113 | ||
![]() |
0f7f243b98 | ||
![]() |
97d7996a50 | ||
![]() |
b658d164f9 | ||
![]() |
f150ae478b | ||
![]() |
d1f68553f1 | ||
![]() |
f47f0800de | ||
![]() |
b7ddefc950 | ||
![]() |
25b3325a44 | ||
![]() |
c281b87bab | ||
![]() |
c0946ddda2 | ||
![]() |
1128cf679c | ||
![]() |
ff65a42350 | ||
![]() |
c459587dcd | ||
![]() |
6f8ea9677f | ||
![]() |
38197527fa | ||
![]() |
21b2bd2c8e | ||
![]() |
25158eee55 | ||
![]() |
1aa804f255 | ||
![]() |
fbe101339d | ||
![]() |
a4aeb8171d | ||
![]() |
27f98a459c |
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"manifest_version": 4,
|
||||
"type": "extension",
|
||||
"name": "LLOneBot v3.26.2",
|
||||
"name": "LLOneBot v3.27.4",
|
||||
"slug": "LLOneBot",
|
||||
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
|
||||
"version": "3.26.2",
|
||||
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发",
|
||||
"version": "3.27.4",
|
||||
"icon": "./icon.jpg",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -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"
|
||||
|
@@ -43,6 +43,7 @@ export class ConfigUtil {
|
||||
enableQOAutoQuote: false
|
||||
}
|
||||
let defaultConfig: Config = {
|
||||
enableLLOB: true,
|
||||
ob11: ob11Default,
|
||||
heartInterval: 60000,
|
||||
token: '',
|
||||
|
@@ -17,6 +17,7 @@ export interface CheckVersion {
|
||||
version: string
|
||||
}
|
||||
export interface Config {
|
||||
enableLLOB: boolean
|
||||
ob11: OB11Config
|
||||
token?: string
|
||||
heartInterval?: number // ms
|
||||
|
@@ -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'
|
@@ -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()
|
||||
}
|
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
178
src/main/main.ts
178
src/main/main.ts
@@ -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,43 @@ 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)
|
||||
}
|
||||
})
|
||||
// OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
|
||||
// log(message)
|
||||
// if (friendAddEvent) {
|
||||
// // log("post friend add event", friendAddEvent);
|
||||
// postOb11Event(friendAddEvent)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
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(selfInfo.uin), parseInt(id))
|
||||
}
|
||||
postOb11Event(pokeEvent)
|
||||
})
|
||||
}
|
||||
}
|
||||
registerReceiveHook<{
|
||||
msgList: Array<RawMessage>
|
||||
@@ -226,6 +241,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)) {
|
||||
@@ -299,34 +318,36 @@ function onLoad() {
|
||||
// if (notify.user2.uid) {
|
||||
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
|
||||
// }
|
||||
if (
|
||||
[GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
|
||||
notify.type,
|
||||
)
|
||||
) {
|
||||
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
|
||||
log('有管理员变动通知')
|
||||
refreshGroupMembers(notify.group.groupCode).then()
|
||||
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
|
||||
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
|
||||
log('开始获取变动的管理员')
|
||||
if (member1) {
|
||||
log('变动管理员获取成功')
|
||||
groupAdminNoticeEvent.user_id = parseInt(member1.uin)
|
||||
groupAdminNoticeEvent.sub_type = [
|
||||
GroupNotifyTypes.ADMIN_UNSET,
|
||||
GroupNotifyTypes.ADMIN_UNSET_OTHER,
|
||||
].includes(notify.type)
|
||||
? 'unset'
|
||||
: 'set'
|
||||
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
|
||||
postOb11Event(groupAdminNoticeEvent, true)
|
||||
}
|
||||
else {
|
||||
log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
|
||||
}
|
||||
}
|
||||
else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
|
||||
// 原本的群管变更通知事件处理
|
||||
// if (
|
||||
// [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
|
||||
// notify.type,
|
||||
// )
|
||||
// ) {
|
||||
// const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
|
||||
// log('有管理员变动通知')
|
||||
// refreshGroupMembers(notify.group.groupCode).then()
|
||||
// let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
|
||||
// groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
|
||||
// log('开始获取变动的管理员')
|
||||
// if (member1) {
|
||||
// log('变动管理员获取成功')
|
||||
// groupAdminNoticeEvent.user_id = parseInt(member1.uin)
|
||||
// groupAdminNoticeEvent.sub_type = [
|
||||
// GroupNotifyTypes.ADMIN_UNSET,
|
||||
// GroupNotifyTypes.ADMIN_UNSET_OTHER,
|
||||
// ].includes(notify.type)
|
||||
// ? 'unset'
|
||||
// : 'set'
|
||||
// // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
|
||||
// postOb11Event(groupAdminNoticeEvent, true)
|
||||
// }
|
||||
// else {
|
||||
// log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
|
||||
log('有成员退出通知', notify)
|
||||
try {
|
||||
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid)
|
||||
@@ -349,32 +370,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 +450,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 +462,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)
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -5,17 +5,51 @@ import { deleteGroup, uidMaps } from '../../common/data'
|
||||
import { dbUtil } from '../../common/db'
|
||||
import { log } from '../../common/utils/log'
|
||||
import { NTQQWindowApi, NTQQWindows } from './window'
|
||||
import { wrapperApi } from '../native/wrapper'
|
||||
|
||||
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 +92,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 +205,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 +217,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>({
|
||||
@@ -233,4 +283,28 @@ export class NTQQGroupApi {
|
||||
})
|
||||
}
|
||||
static publishGroupBulletin(groupQQ: string, title: string, content: string) {}
|
||||
static async removeGroupEssence(GroupCode: string, msgId: string) {
|
||||
// 代码没测过
|
||||
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
|
||||
let MsgData = await wrapperApi.NodeIQQNTWrapperSession.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
|
||||
let param = {
|
||||
groupCode: GroupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
|
||||
};
|
||||
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
|
||||
return wrapperApi.NodeIQQNTWrapperSession.getGroupService().removeGroupEssence(param);
|
||||
}
|
||||
static async addGroupEssence(GroupCode: string, msgId: string) {
|
||||
// 代码没测过
|
||||
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
|
||||
let MsgData = await wrapperApi.NodeIQQNTWrapperSession.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
|
||||
let param = {
|
||||
groupCode: GroupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
|
||||
};
|
||||
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
|
||||
return wrapperApi.NodeIQQNTWrapperSession.getGroupService().addGroupEssence(param);
|
||||
}
|
||||
}
|
||||
|
@@ -1,19 +1,16 @@
|
||||
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
|
||||
import { ChatType, RawMessage, SendMessageElement } from '../types'
|
||||
import { ChatType, RawMessage, SendMessageElement, Peer } from '../types'
|
||||
import { dbUtil } from '../../common/db'
|
||||
import { selfInfo } from '../../common/data'
|
||||
import { ReceiveCmdS, registerReceiveHook } from '../hook'
|
||||
import { log } from '../../common/utils/log'
|
||||
import { sleep } from '../../common/utils/helper'
|
||||
import { isQQ998 } from '../../common/utils'
|
||||
import { wrapperApi } from '@/ntqqapi/native/wrapper'
|
||||
|
||||
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunnc
|
||||
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc
|
||||
|
||||
export interface Peer {
|
||||
chatType: ChatType
|
||||
peerUid: string // 如果是群聊uid为群号,私聊uid就是加密的字符串
|
||||
guildId?: ''
|
||||
}
|
||||
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage
|
||||
|
||||
async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) {
|
||||
// 等待上一个相同的peer发送完
|
||||
@@ -29,7 +26,8 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
|
||||
await sleep(500)
|
||||
checkLastSendUsingTime += 500
|
||||
return await waitLastSend()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -39,16 +37,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 +62,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 +103,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 +118,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 +140,7 @@ export class NTQQMsgApi {
|
||||
args: [{ peer, cnt: 20 }, null],
|
||||
})
|
||||
}
|
||||
|
||||
static async activateChatAndGetHistory(peer: Peer) {
|
||||
// await this.fetchRecentContact();
|
||||
// await sleep(500);
|
||||
@@ -114,6 +150,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 +166,7 @@ export class NTQQMsgApi {
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
static async fetchRecentContact() {
|
||||
await callNTQQApi({
|
||||
methodName: NTQQApiMethod.RECENT_CONTACT,
|
||||
@@ -164,7 +202,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 +215,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 +233,7 @@ export class NTQQMsgApi {
|
||||
null,
|
||||
],
|
||||
}).then().catch(log)
|
||||
return await waiter;
|
||||
return await waiter
|
||||
}
|
||||
|
||||
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
|
||||
@@ -250,4 +288,7 @@ export class NTQQMsgApi {
|
||||
})
|
||||
})
|
||||
}
|
||||
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
|
||||
return await wrapperApi.NodeIQQNTWrapperSession.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
|
||||
}
|
||||
}
|
||||
|
@@ -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失败')
|
||||
}
|
||||
|
@@ -22,6 +22,8 @@ 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'
|
||||
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
|
||||
|
||||
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
|
||||
|
||||
@@ -324,207 +326,229 @@ 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),
|
||||
)
|
||||
} else if (member.role != existMember.role) {
|
||||
log('有管理员变动通知')
|
||||
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
|
||||
groupAdminNoticeEvent.group_id = parseInt(groupCode)
|
||||
groupAdminNoticeEvent.user_id = parseInt(member.uin)
|
||||
groupAdminNoticeEvent.sub_type = member.role == GroupMemberRole.admin ? 'set' : 'unset'
|
||||
postOb11Event(groupAdminNoticeEvent, true)
|
||||
}
|
||||
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 })
|
||||
})
|
||||
})
|
||||
|
||||
}
|
@@ -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 },
|
||||
|
@@ -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, 没有host,host是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
|
||||
}
|
||||
}
|
||||
@@ -413,3 +415,9 @@ export interface RawMessage {
|
||||
multiForwardMsgElement: MultiForwardMsgElement
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface Peer {
|
||||
chatType: ChatType;
|
||||
peerUid: string; // 如果是群聊uid为群号,私聊uid就是加密的字符串
|
||||
guildId?: string;
|
||||
}
|
@@ -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,
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
24
src/onebot11/action/go-cqhttp/DelEssenceMsg.ts
Normal file
24
src/onebot11/action/go-cqhttp/DelEssenceMsg.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
|
||||
import { dbUtil } from '@/common/db';
|
||||
|
||||
interface Payload {
|
||||
message_id: number | string;
|
||||
}
|
||||
|
||||
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
|
||||
actionName = ActionName.GoCQHTTP_DelEssenceMsg;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<any> {
|
||||
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
|
||||
if (!msg) {
|
||||
throw new Error('msg not found');
|
||||
}
|
||||
return await NTQQGroupApi.removeGroupEssence(
|
||||
msg.peerUid,
|
||||
msg.msgId
|
||||
);
|
||||
}
|
||||
}
|
23
src/onebot11/action/go-cqhttp/SetEssenceMsg.ts
Normal file
23
src/onebot11/action/go-cqhttp/SetEssenceMsg.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
|
||||
import { dbUtil } from '@/common/db';
|
||||
|
||||
interface Payload {
|
||||
message_id: number | string;
|
||||
}
|
||||
|
||||
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
|
||||
actionName = ActionName.GoCQHTTP_SetEssenceMsg;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<any> {
|
||||
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
|
||||
if (!msg) {
|
||||
throw new Error('msg not found');
|
||||
}
|
||||
return await NTQQGroupApi.addGroupEssence(
|
||||
msg.peerUid,
|
||||
msg.msgId
|
||||
);
|
||||
}
|
||||
}
|
@@ -46,10 +46,14 @@ 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'
|
||||
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
|
||||
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
|
||||
import GetEvent from './llonebot/GetEvent'
|
||||
|
||||
|
||||
export const actionHandlers = [
|
||||
new GetFile(),
|
||||
@@ -59,6 +63,7 @@ export const actionHandlers = [
|
||||
new GetGroupAddRequest(),
|
||||
new SetQQAvatar(),
|
||||
new GetFriendWithCategory(),
|
||||
new GetEvent(),
|
||||
// onebot11
|
||||
new SendLike(),
|
||||
new GetMsg(),
|
||||
@@ -91,7 +96,7 @@ export const actionHandlers = [
|
||||
new GetCookies(),
|
||||
new SetMsgEmojiLike(),
|
||||
new ForwardFriendSingleMsg(),
|
||||
new ForwardSingleGroupMsg(),
|
||||
new ForwardGroupSingleMsg(),
|
||||
//以下为go-cqhttp api
|
||||
new GetGroupEssence(),
|
||||
new GetGroupHonorInfo(),
|
||||
@@ -106,7 +111,9 @@ export const actionHandlers = [
|
||||
new GoCQHTTPUploadPrivateFile(),
|
||||
new GoCQHTTPGetGroupMsgHistory(),
|
||||
new GoCQHTTGetForwardMsgAction(),
|
||||
new GoCQHTTHandleQuickOperation()
|
||||
new GoCQHTTHandleQuickOperation(),
|
||||
new GoCQHTTPSetEssenceMsg(),
|
||||
new GoCQHTTPDelEssenceMsg()
|
||||
]
|
||||
|
||||
function initActionMap() {
|
||||
|
23
src/onebot11/action/llonebot/GetEvent.ts
Normal file
23
src/onebot11/action/llonebot/GetEvent.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import BaseAction from '../BaseAction'
|
||||
import { ActionName } from '../types'
|
||||
import { getHttpEvent } from '../../server/event-for-http'
|
||||
import { PostEventType } from '../../server/post-ob11-event'
|
||||
// import { log } from "../../../common/utils";
|
||||
|
||||
interface Payload {
|
||||
key: string
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export default class GetEvent extends BaseAction<Payload, PostEventType[]> {
|
||||
actionName = ActionName.GetEvent
|
||||
protected async _handle(payload: Payload): Promise<PostEventType[]> {
|
||||
let key = ''
|
||||
if (payload.key) {
|
||||
key = payload.key;
|
||||
}
|
||||
let timeout = parseInt(payload.timeout?.toString()) || 0;
|
||||
let evts = await getHttpEvent(key,timeout);
|
||||
return evts;
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -22,6 +22,7 @@ export enum ActionName {
|
||||
Debug = 'llonebot_debug',
|
||||
GetFile = 'get_file',
|
||||
GetFriendsWithCategory = 'get_friends_with_category',
|
||||
GetEvent = 'get_event',
|
||||
// onebot 11
|
||||
SendLike = 'send_like',
|
||||
GetLoginInfo = 'get_login_info',
|
||||
@@ -70,4 +71,6 @@ export enum ActionName {
|
||||
GoCQHTTP_GetEssenceMsg = "get_essence_msg_list",
|
||||
GoCQHTTP_HandleQuickOperation = ".handle_quick_operation",
|
||||
GetGroupHonorInfo = "get_group_honor_info",
|
||||
GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
|
||||
GoCQHTTP_DelEssenceMsg = 'delete_essence_msg',
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
FaceIndex,
|
||||
GrayTipElementSubType,
|
||||
Group,
|
||||
Peer,
|
||||
GroupMember,
|
||||
PicType,
|
||||
RawMessage,
|
||||
@@ -24,7 +25,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'
|
||||
@@ -34,6 +35,7 @@ import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNotice
|
||||
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
|
||||
import { NTQQUserApi } from '../ntqqapi/api/user'
|
||||
import { NTQQFileApi } from '../ntqqapi/api/file'
|
||||
import { NTQQMsgApi } from '../ntqqapi/api/msg'
|
||||
import { calcQQLevel } from '../common/utils/qqlevel'
|
||||
import { log } from '../common/utils/log'
|
||||
import { sleep } from '../common/utils/helper'
|
||||
@@ -47,6 +49,9 @@ 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';
|
||||
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent';
|
||||
|
||||
let lastRKeyUpdateTime = 0
|
||||
|
||||
@@ -107,33 +112,40 @@ export class OB11Constructor {
|
||||
}
|
||||
|
||||
for (let element of msg.elements) {
|
||||
let message_data: OB11MessageData | any = {
|
||||
data: {},
|
||||
type: 'unknown',
|
||||
let message_data: OB11MessageData = {
|
||||
data: {} as any,
|
||||
type: 'unknown' as any,
|
||||
}
|
||||
if (element.textElement && element.textElement?.atType !== AtType.notAt) {
|
||||
message_data['type'] = OB11MessageDataType.at
|
||||
let qq: string
|
||||
let name: string | undefined
|
||||
if (element.textElement.atType == AtType.atAll) {
|
||||
// message_data["data"]["mention"] = "all"
|
||||
message_data['data']['qq'] = 'all'
|
||||
qq = 'all'
|
||||
}
|
||||
else {
|
||||
let atUid = element.textElement.atNtUid
|
||||
const { atNtUid, content } = element.textElement
|
||||
let atQQ = element.textElement.atUid
|
||||
if (!atQQ || atQQ === '0') {
|
||||
const atMember = await getGroupMember(msg.peerUin, atUid)
|
||||
const atMember = await getGroupMember(msg.peerUin, atNtUid)
|
||||
if (atMember) {
|
||||
atQQ = atMember.uin
|
||||
}
|
||||
}
|
||||
if (atQQ) {
|
||||
// message_data["data"]["mention"] = atQQ
|
||||
message_data['data']['qq'] = atQQ
|
||||
qq = atQQ
|
||||
name = content.replace('@', '')
|
||||
}
|
||||
}
|
||||
message_data = {
|
||||
type: OB11MessageDataType.at,
|
||||
data: {
|
||||
qq,
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (element.textElement) {
|
||||
message_data['type'] = 'text'
|
||||
message_data['type'] = OB11MessageDataType.text
|
||||
let text = element.textElement.content
|
||||
if (!text.trim()) {
|
||||
continue
|
||||
@@ -141,7 +153,7 @@ export class OB11Constructor {
|
||||
message_data['data']['text'] = text
|
||||
}
|
||||
else if (element.replyElement) {
|
||||
message_data['type'] = 'reply'
|
||||
message_data['type'] = OB11MessageDataType.reply
|
||||
// log("收到回复消息", element.replyElement.replayMsgSeq)
|
||||
try {
|
||||
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
|
||||
@@ -157,14 +169,16 @@ export class OB11Constructor {
|
||||
}
|
||||
}
|
||||
else if (element.picElement) {
|
||||
message_data['type'] = 'image'
|
||||
message_data['type'] = OB11MessageDataType.image
|
||||
// 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 +202,7 @@ export class OB11Constructor {
|
||||
element.picElement.sourcePath,
|
||||
)
|
||||
},
|
||||
})
|
||||
.then()
|
||||
// 不在自动下载图片
|
||||
}).then()
|
||||
}
|
||||
else if (element.videoElement || element.fileElement) {
|
||||
const videoOrFileElement = element.videoElement || element.fileElement
|
||||
@@ -200,6 +212,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,
|
||||
@@ -287,10 +306,10 @@ export class OB11Constructor {
|
||||
message_data['type'] = OB11MessageDataType.forward
|
||||
message_data['data']['id'] = msg.msgId
|
||||
}
|
||||
if (message_data.type !== 'unknown' && message_data.data) {
|
||||
if ((message_data.type as string) !== '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 +320,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事件
|
||||
const pokedetail: any[] = json.items;
|
||||
//筛选item带有uid的元素
|
||||
const poke_uid = pokedetail.filter(item => item.uid);
|
||||
if (poke_uid.length == 2) {
|
||||
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail);
|
||||
}
|
||||
}
|
||||
//下面得改 上面也是错的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,29 +533,51 @@ export class OB11Constructor {
|
||||
}
|
||||
|
||||
* */
|
||||
const memberUin = json.items[1].param[0]
|
||||
const title = json.items[3].txt
|
||||
log('收到群成员新头衔消息', json)
|
||||
getGroupMember(msg.peerUid, memberUin).then((member) => {
|
||||
member.memberSpecialTitle = title
|
||||
})
|
||||
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
|
||||
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
|
||||
//判断业务类型
|
||||
//Poke事件
|
||||
const pokedetail: any[] = json.items;
|
||||
//筛选item带有uid的元素
|
||||
const poke_uid = pokedetail.filter(item => item.uid);
|
||||
if (poke_uid.length == 2) {
|
||||
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail);
|
||||
}
|
||||
}
|
||||
if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
|
||||
log('收到群精华消息', json)
|
||||
const searchParams = new URL(json.items[0].jp).searchParams;
|
||||
const msgSeq = searchParams.get('msgSeq')!;
|
||||
const Group = searchParams.get('groupCode');
|
||||
const Businessid = searchParams.get('businessid');
|
||||
const Peer: Peer = {
|
||||
guildId: '',
|
||||
chatType: ChatType.group,
|
||||
peerUid: Group!
|
||||
};
|
||||
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList;
|
||||
const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId);
|
||||
const postMsg = await dbUtil.getMsgBySeqId(origMsg.msgSeq) ?? origMsg;
|
||||
// 如果 senderUin 为 0,可能是 历史消息 或 自身消息
|
||||
if (msgList[0].senderUin === '0') {
|
||||
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin;
|
||||
}
|
||||
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg.msgShortId, parseInt(msgList[0].senderUin));
|
||||
// 获取MsgSeq+Peer可获取具体消息
|
||||
}
|
||||
if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
|
||||
const memberUin = json.items[1].param[0]
|
||||
const title = json.items[3].txt
|
||||
log('收到群成员新头衔消息', json)
|
||||
getGroupMember(msg.peerUid, memberUin).then((member) => {
|
||||
member.memberSpecialTitle = title
|
||||
})
|
||||
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
|
@@ -60,7 +60,15 @@ export function encodeCQCode(data: OB11MessageData) {
|
||||
let result = '[CQ:' + data.type
|
||||
for (const name in data.data) {
|
||||
const value = data.data[name]
|
||||
result += `,${name}=${CQCodeEscape(value)}`
|
||||
if (value === undefined) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const text = value.toString()
|
||||
result += `,${name}=${CQCodeEscape(text)}`
|
||||
} catch (error) {
|
||||
// If it can't be converted, skip this name-value pair
|
||||
}
|
||||
}
|
||||
result += ']'
|
||||
return result
|
||||
|
14
src/onebot11/event/notice/OB11GroupEssenceEvent.ts
Normal file
14
src/onebot11/event/notice/OB11GroupEssenceEvent.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
export class OB11GroupEssenceEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'essence';
|
||||
message_id: number;
|
||||
sender_id: number;
|
||||
sub_type: 'add' | 'delete' = 'add';
|
||||
|
||||
constructor(groupId: number, message_id: number, sender_id: number) {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.message_id = message_id;
|
||||
this.sender_id = sender_id;
|
||||
}
|
||||
}
|
@@ -5,26 +5,28 @@ import { OB11BaseEvent } from '../OB11BaseEvent'
|
||||
class OB11PokeEvent extends OB11BaseNoticeEvent {
|
||||
notice_type = 'notify'
|
||||
sub_type = 'poke'
|
||||
target_id = parseInt(selfInfo.uin)
|
||||
target_id = 0
|
||||
user_id: number
|
||||
raw_message: any
|
||||
}
|
||||
|
||||
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, raw_message: any) {
|
||||
super();
|
||||
this.target_id = target_id;
|
||||
this.user_id = user_id;
|
||||
this.raw_message = raw_message;
|
||||
}
|
||||
}
|
||||
|
||||
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, raw_message: any) {
|
||||
super()
|
||||
this.group_id = group_id
|
||||
this.target_id = user_id
|
||||
this.target_id = target_id
|
||||
this.user_id = user_id
|
||||
this.raw_message = raw_message
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
68
src/onebot11/server/event-for-http.ts
Normal file
68
src/onebot11/server/event-for-http.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { PostEventType } from "./post-ob11-event"
|
||||
|
||||
|
||||
interface HttpEventType {
|
||||
seq: number
|
||||
event: PostEventType
|
||||
}
|
||||
|
||||
interface HttpUserType {
|
||||
lastAccessTime: number
|
||||
userSeq: number
|
||||
}
|
||||
|
||||
let curentSeq:number = 0;
|
||||
let eventList:HttpEventType[] = [];
|
||||
let httpUser:Record<string,HttpUserType> = {};
|
||||
|
||||
|
||||
export function postHttpEvent(event: PostEventType) {
|
||||
curentSeq += 1;
|
||||
eventList.push({
|
||||
seq: curentSeq,
|
||||
event: event
|
||||
});
|
||||
while(eventList.length > 100) {
|
||||
eventList.shift();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function getHttpEvent(userKey:string,timeout = 0) {
|
||||
let toRetEvent = [];
|
||||
|
||||
// 清除过时的user,5分钟没访问过的user将被删除
|
||||
let now = Date.now();
|
||||
for(let key in httpUser) {
|
||||
let user = httpUser[key];
|
||||
if(now - user.lastAccessTime > 1000 * 60 * 5) {
|
||||
delete httpUser[key];
|
||||
}
|
||||
}
|
||||
|
||||
// 增加新的user
|
||||
if(!httpUser[userKey] ) {
|
||||
httpUser[userKey] = {
|
||||
lastAccessTime: now,
|
||||
userSeq: curentSeq
|
||||
}
|
||||
}
|
||||
|
||||
let user = httpUser[userKey];
|
||||
// 等待数据到来,暂时先这么写吧......
|
||||
while(curentSeq == user.userSeq && Date.now() - now < timeout) {
|
||||
await new Promise( resolve => setTimeout(resolve, 10) );
|
||||
}
|
||||
// 取数据
|
||||
for(let i = 0; i < eventList.length; i++) {
|
||||
let evt = eventList[i];
|
||||
if(evt.seq > user.userSeq) {
|
||||
toRetEvent.push(evt.event);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新user数据
|
||||
user.lastAccessTime = Date.now();
|
||||
user.userSeq = curentSeq;
|
||||
return toRetEvent;
|
||||
}
|
@@ -8,6 +8,7 @@ import { log } from '@/common/utils'
|
||||
import { getConfigUtil } from '@/common/config'
|
||||
import crypto from 'crypto'
|
||||
import { handleQuickOperation, QuickOperationEvent } from '../action/quick-operation'
|
||||
import { postHttpEvent } from './event-for-http'
|
||||
|
||||
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
|
||||
|
||||
@@ -78,4 +79,9 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
|
||||
if (postWs) {
|
||||
postWsEvent(msg)
|
||||
}
|
||||
if(!(msg.post_type == 'meta_event' && (msg as OB11BaseMetaEvent).meta_event_type == 'heartbeat')) {
|
||||
// 不上报心跳
|
||||
postHttpEvent(msg)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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))
|
||||
|
@@ -195,6 +195,7 @@ export interface OB11MessageAt {
|
||||
type: OB11MessageDataType.at
|
||||
data: {
|
||||
qq: string | 'all'
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +242,20 @@ export interface OB11MessageJson {
|
||||
data: { data: string /* , config: { token: string } */ }
|
||||
}
|
||||
|
||||
export interface OB11MessageMarkdown {
|
||||
type: OB11MessageDataType.markdown
|
||||
data: {
|
||||
data: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface OB11MessageForward {
|
||||
type: OB11MessageDataType.forward
|
||||
data: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export type OB11MessageData =
|
||||
| OB11MessageText
|
||||
| OB11MessageFace
|
||||
@@ -258,6 +273,8 @@ export type OB11MessageData =
|
||||
| OB11MessagePoke
|
||||
| OB11MessageDice
|
||||
| OB11MessageRPS
|
||||
| OB11MessageMarkdown
|
||||
| OB11MessageForward
|
||||
|
||||
export interface OB11PostSendMsg {
|
||||
message_type?: 'private' | 'group'
|
||||
|
@@ -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 服务',
|
||||
|
@@ -1 +1 @@
|
||||
export const version = '3.26.2'
|
||||
export const version = '3.27.4'
|
||||
|
Reference in New Issue
Block a user