Compare commits

..

61 Commits

Author SHA1 Message Date
idranme
85001a40da Merge pull request #366 from LLOneBot/dev
3.30.4
2024-08-23 17:05:03 +08:00
idranme
867a05c85a chore: v3.30.4 2024-08-23 17:03:58 +08:00
idranme
d8a63f6561 fix 2024-08-23 17:02:31 +08:00
idranme
e9fb9d1b30 Update publish.yml 2024-08-23 16:08:59 +08:00
idranme
b4fc987537 Merge pull request #365 from LLOneBot/dev
3.30.3
2024-08-23 13:40:59 +08:00
idranme
d0ccf53d88 chore: v3.30.3 2024-08-23 13:39:26 +08:00
idranme
d5ca94569d fix 2024-08-23 13:32:58 +08:00
idranme
bf72685501 Merge pull request #363 from LLOneBot/dev
3.30.2
2024-08-23 00:30:48 +08:00
idranme
c07467b670 chore: v3.30.2 2024-08-23 00:08:52 +08:00
idranme
ea164fb048 fix: friend list 2024-08-22 23:47:15 +08:00
idranme
0c0ad9a616 Merge pull request #362 from LLOneBot/dev
3.30.1
2024-08-22 20:41:32 +08:00
idranme
7bb4808e2d chore: v3.30.1 2024-08-22 20:18:16 +08:00
idranme
3f7592d06d opt 2024-08-22 20:17:28 +08:00
idranme
2f341fcf43 fix 2024-08-22 18:16:08 +08:00
idranme
9c59e5903e Merge pull request #360 from LLOneBot/dev
3.30.0
2024-08-22 12:41:06 +08:00
idranme
339ba409ee chore: v3.30.0 2024-08-22 12:37:43 +08:00
idranme
099da66661 fix: poke event 2024-08-22 12:32:09 +08:00
idranme
adcde6e49e fix 2024-08-22 06:37:28 +08:00
idranme
b3b8f9cd72 fix 2024-08-22 06:23:35 +08:00
idranme
8b57ebd7de fix: adaptation 27187 2024-08-22 05:45:02 +08:00
idranme
1afaeb0396 fix: adaptation 27187 2024-08-22 03:34:42 +08:00
idranme
235a986253 fix: adaptation 27187 2024-08-22 02:48:01 +08:00
idranme
b16bea9548 fix: adaptation 27187 2024-08-22 02:01:44 +08:00
idranme
7897034d13 opt 2024-08-22 00:42:12 +08:00
idranme
eabe891838 opt 2024-08-21 23:36:35 +08:00
idranme
75d3fc27f0 chore: remove unused methods 2024-08-21 22:51:00 +08:00
idranme
111bb4dd88 fix: adaptation 27187 2024-08-21 22:14:52 +08:00
idranme
f8bf60a3a0 Merge pull request #357 from cnxysoft/dev
fix: Linux上报
2024-08-21 17:50:42 +08:00
Alen
7c22eb3376 fix: Linux上报 2024-08-21 17:42:33 +08:00
Alen
7e1f7ac7f5 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-21 10:56:00 +08:00
idranme
4ea02676f7 Merge pull request #354 from LLOneBot/dev
3.29.6
2024-08-21 00:29:41 +08:00
idranme
ddefb4c194 chore: v3.29.6 2024-08-21 00:27:47 +08:00
idranme
2792fa4776 fix 2024-08-21 00:14:15 +08:00
idranme
c37858e2f9 opt 2024-08-20 21:13:27 +08:00
idranme
59a11faa7f Merge pull request #352 from LLOneBot/dev
3.29.5
2024-08-19 17:40:30 +08:00
idranme
3b3795c946 chore: v3.29.5 2024-08-19 17:38:42 +08:00
idranme
ff18937828 fix 2024-08-19 17:29:58 +08:00
idranme
65d02d7f21 Merge pull request #351 from LLOneBot/main
merge
2024-08-19 12:59:10 +08:00
idranme
9cb8ba017e Merge pull request #350 from snsin09/nocache
ws修复必须no_cache参数
2024-08-19 12:55:27 +08:00
yota
1e579858b8 ws修复必须no_cache参数 2024-08-19 09:47:24 +08:00
idranme
db0c800851 Merge pull request #347 from LLOneBot/dev
3.29.4
2024-08-18 21:09:15 +08:00
idranme
e912911dd8 chore: v3.29.4 2024-08-18 21:04:30 +08:00
idranme
2245d0d3de fix 2024-08-18 20:58:26 +08:00
idranme
a56eac0251 Merge pull request #345 from LLOneBot/main
merge
2024-08-18 16:45:02 +08:00
linyuchen
8be0562c19 Merge pull request #344 from LLOneBot/linyuchen-patch-1
Fix: typo
2024-08-17 23:46:12 +08:00
linyuchen
f4c77f3e20 Fix: typo 2024-08-17 23:45:41 +08:00
linyuchen
508e6f2928 Merge pull request #342 from gfhdhytghd/patch-1
Update LICENSE
2024-08-17 16:20:50 +08:00
lin
9353cb0432 Update LICENSE
修改许可证以在法律层面上禁止宣传
2024-08-17 14:21:27 +08:00
idranme
816e07f47c Merge pull request #341 from LLOneBot/dev
3.29.3
2024-08-16 22:27:41 +08:00
idranme
46b1e8e67d chore: v3.29.3 2024-08-16 22:25:17 +08:00
idranme
8542594181 fix 2024-08-16 21:58:05 +08:00
idranme
0d7aa9bd2c fix 2024-08-16 21:28:43 +08:00
idranme
a47ee4c3e4 fix 2024-08-16 09:53:23 +08:00
Alen
4efcf5b520 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 19:56:48 +08:00
Alen
9ff6ff7cab Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 16:09:24 +08:00
Alen
594a421163 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-09 22:15:54 +08:00
Alen
b748d84e8a Merge branch 'dev' of https://github.com/cnxysoft/LLOneBot into dev 2024-08-07 15:06:19 +08:00
Alen
e8d83d2958 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-07 15:06:11 +08:00
Alen
cdb34ffe61 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 22:15:48 +08:00
Alen
a45c56bd85 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 10:09:12 +08:00
Alen
bb07ebd5d7 Merge branch 'main' into dev 2024-08-05 10:07:28 +08:00
54 changed files with 1651 additions and 1564 deletions

View File

@@ -14,7 +14,7 @@ jobs:
- name: setup node - name: setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
- name: install dependenies - name: install dependenies
run: | run: |

View File

@@ -1,4 +1,4 @@
MIT License MIT Without Public Sicial Media Promotion License
Copyright (c) 2024 LLOneBot Copyright (c) 2024 LLOneBot
@@ -19,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
You may use this software in accordance with the above terms, but you are not
allowed to promote this project or your projects based on this project on any
public social media.

View File

@@ -1,6 +1,6 @@
# LLOneBot # LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** > **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**

View File

@@ -3,8 +3,8 @@
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用 QQ 机器人开发", "description": "实现 OneBot 11 协议,用 QQ 机器人开发",
"version": "3.29.2", "version": "3.30.4",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -16,15 +16,16 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.4.1", "@minatojs/driver-sqlite": "^4.5.0",
"compressing": "^1.10.1", "compressing": "^1.10.1",
"cordis": "^3.17.9", "cordis": "^3.18.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.2",
"express": "^4.19.2", "express": "^4.19.2",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^4.4.1",
"file-type": "^19.4.0", "file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.4.3", "minato": "^3.5.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
@@ -34,10 +35,10 @@
"@types/fluent-ffmpeg": "^2.1.25", "@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.14.15", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^29.1.4", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.0", "vite": "^5.4.2",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.0" "packageManager": "yarn@4.4.0"

View File

@@ -6,7 +6,7 @@ const manifest = {
type: 'extension', type: 'extension',
name: 'LLOneBot', name: 'LLOneBot',
slug: 'LLOneBot', slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用 QQ 机器人开发', description: '实现 OneBot 11 协议,用 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [

View File

@@ -1,17 +0,0 @@
import { Level } from 'level'
const db = new Level(process.env['level_db_path'] as string, { valueEncoding: 'json' })
async function getGroupNotify() {
let keys = await db.keys().all()
let result: string[] = []
for (const key of keys) {
// console.log(key)
if (key.startsWith('group_notify_')) {
result.push(key)
}
}
return result
}
getGroupNotify().then(console.log)

View File

@@ -5,9 +5,7 @@ import path from 'node:path'
import { getSelfUin } from './data' import { getSelfUin } from './data'
import { DATA_DIR } from './utils' import { DATA_DIR } from './utils'
export const HOOK_LOG = false //export const HOOK_LOG = false
export const ALLOW_SEND_TEMP_MSG = false
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string private readonly configPath: string

View File

@@ -1,6 +1,5 @@
import { import {
type Friend, type Friend,
type Group,
type GroupMember, type GroupMember,
type SelfInfo, type SelfInfo,
} from '../ntqqapi/types' } from '../ntqqapi/types'
@@ -11,8 +10,8 @@ import { isNumeric } from './utils/helper'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api' import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types' import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config' import { getConfigUtil } from './config'
import { getBuildVersion } from './utils/QQBasicInfo'
export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
@@ -24,10 +23,10 @@ export const llonebotError: LLOneBotError = {
export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>() export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> { export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid' const filterKey: 'uin' | 'uid' = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
let filterValue = uinOrUid const filterValue = uinOrUid
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString()) let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend) { if (!friend && getBuildVersion() < 26702) {
try { try {
const _friends = await NTQQFriendApi.getFriends(true) const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString()) friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
@@ -41,39 +40,15 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
return friend return friend
} }
export async function getGroup(qq: string): Promise<Group | undefined> { export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
let group = groups.find((group) => group.groupCode === qq.toString()) const groupCodeStr = groupCode.toString()
if (!group) { const memberUinOrUidStr = memberUinOrUid.toString()
try { let members = groupMembers.get(groupCodeStr)
const _groups = await NTQQGroupApi.getGroups(true)
group = _groups.find((group) => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
}
return group
}
export function deleteGroup(groupCode: string) {
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString())
// log(groups, groupCode, groupIndex);
if (groupIndex !== -1) {
log('删除群', groupCode)
groups.splice(groupIndex, 1)
}
}
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
let members = groupMembers.get(groupQQ)
if (!members) { if (!members) {
try { try {
members = await NTQQGroupApi.getGroupMembers(groupQQ) members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表 // 更新群成员列表
groupMembers.set(groupQQ, members) groupMembers.set(groupCodeStr, members)
} }
catch (e) { catch (e) {
return null return null
@@ -81,16 +56,17 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s
} }
const getMember = () => { const getMember = () => {
let member: GroupMember | undefined = undefined let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUid)) { if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUid) member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
} else { } else {
member = members!.get(memberUinOrUid) member = members!.get(memberUinOrUidStr)
} }
return member return member
} }
let member = getMember() let member = getMember()
if (!member) { if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupQQ) members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember() member = getMember()
} }
return member return member
@@ -132,11 +108,10 @@ export function getSelfUin() {
} }
const messages: Map<string, RawMessage> = new Map() const messages: Map<string, RawMessage> = new Map()
let expire: number
/** 缓存近期消息内容 */ /** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) { export async function addMsgCache(msg: RawMessage) {
expire ??= getConfigUtil().getConfig().msgCacheExpire! * 1000 const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) { if (expire === 0) {
return return
} }

View File

@@ -100,7 +100,7 @@ export abstract class HttpServerBase {
} else if (req.query) { } else if (req.query) {
payload = { ...req.query, ...req.body } payload = { ...req.query, ...req.body }
} }
log('收到http请求', url, payload) log('收到 HTTP 请求', url, payload)
try { try {
res.send(await handler(res, payload)) res.send(await handler(res, payload))
} catch (e: any) { } catch (e: any) {

View File

@@ -1,93 +0,0 @@
import { WebSocket, WebSocketServer } from 'ws'
import urlParse from 'url'
import { IncomingMessage } from 'node:http'
import { log } from '../utils/log'
import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
class WebsocketClientBase {
private wsClient: WebSocket | undefined
constructor() { }
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg)
}
}
onMessage(msg: string) { }
}
export class WebsocketServerBase {
private ws: WebSocketServer | null = null
constructor() {
console.log(`llonebot websocket service started`)
}
start(port: number) {
try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = ''
} catch (e: any) {
llonebotError.wsServerError = '正向ws服务启动失败, ' + e.toString()
}
this.ws?.on('connection', (wsClient, req) => {
const url = req.url?.split('?').shift()
this.authorize(wsClient, req)
this.onConnect(wsClient, url!, req)
wsClient.on('message', async (msg) => {
this.onMessage(wsClient, url!, msg.toString())
})
})
}
stop() {
llonebotError.wsServerError = ''
this.ws?.close((err) => {
log('ws server close failed!', err)
})
this.ws = null
}
restart(port: number) {
this.stop()
this.start(port)
}
authorize(wsClient: WebSocket, req) {
let token = getConfigUtil().getConfig().token
const url = req.url.split('?').shift()
log('ws connect', url)
let clientToken: string = ''
const authHeader = req.headers['authorization']
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()
log('receive ws header token', clientToken)
} else {
const parsedUrl = urlParse.parse(req.url, true)
const urlToken = parsedUrl.query.access_token
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log('receive ws url token', clientToken)
}
}
if (token && clientToken != token) {
this.authorizeFailed(wsClient)
return wsClient.close()
}
}
authorizeFailed(wsClient: WebSocket) { }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { }
onMessage(wsClient: WebSocket, url: string, msg: string) { }
sendHeart() { }
}

View File

@@ -50,3 +50,15 @@ export interface FileCache {
elementId: string elementId: string
elementType: number elementType: number
} }
export interface FileCacheV2 {
fileName: string
fileSize: string
fileUuid: string
msgId: string
msgTime: number
peerUid: string
chatType: number
elementId: string
elementType: number
}

View File

@@ -22,6 +22,7 @@ export class NTEventWrapper {
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例 private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func} private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
public initialised = false
constructor() { constructor() {
} }
@@ -46,6 +47,7 @@ export class NTEventWrapper {
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) { init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession this.WrapperSession = WrapperSession
this.initialised = true
} }
createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined { createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {

View File

@@ -7,7 +7,7 @@ import SQLite from '@minatojs/driver-sqlite'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { FileCache } from '../types' import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables { interface SQLiteTables extends Tables {
message: { message: {
@@ -16,7 +16,7 @@ interface SQLiteTables extends Tables {
chatType: number chatType: number
peerUid: string peerUid: string
} }
file: FileCache file_v2: FileCacheV2
} }
interface MsgIdAndPeerByShortId { interface MsgIdAndPeerByShortId {
@@ -52,16 +52,19 @@ class MessageUniqueWrapper {
}, { }, {
primary: 'shortId' primary: 'shortId'
}) })
database.extend('file', { database.extend('file_v2', {
fileName: 'string', fileName: 'string',
fileSize: 'string', fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)', msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)', peerUid: 'string(24)',
chatType: 'unsigned', chatType: 'unsigned',
elementId: 'string(24)', elementId: 'string(24)',
elementType: 'unsigned', elementType: 'unsigned',
}, { }, {
primary: 'fileName' primary: 'fileUuid',
indexes: ['fileName']
}) })
this.db = database this.db = database
} }
@@ -142,12 +145,18 @@ class MessageUniqueWrapper {
this.msgDataMap.resize(maxSize) this.msgDataMap.resize(maxSize)
} }
addFileCache(data: FileCache) { addFileCache(data: FileCacheV2) {
return this.db?.upsert('file', [data], 'fileName') return this.db?.upsert('file_v2', [data], 'fileUuid')
} }
getFileCache(fileName: string) { getFileCacheByName(fileName: string) {
return this.db?.get('file', { fileName }) return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
} }
} }

View File

@@ -1,9 +1,9 @@
import fs from 'node:fs' import fs from 'node:fs'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { log, TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import * as fileType from 'file-type'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4) const buffer = Buffer.alloc(4)
@@ -32,31 +32,6 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
}) })
} }
export async function file2base64(path: string) {
let result = {
err: '',
data: '',
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000)
} catch (e: any) {
result.err = e.toString()
return result
}
const data = await fsPromise.readFile(path)
// 转换为Base64编码
result.data = data.toString('base64')
} catch (err: any) {
result.err = err.toString()
}
return result
}
export function calculateFileMD5(filePath: string): Promise<string> { export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 创建一个流式读取器 // 创建一个流式读取器
@@ -109,112 +84,118 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
return Buffer.from(await fetchRes.arrayBuffer()) return Buffer.from(await fetchRes.arrayBuffer())
} }
export enum FileUriType {
Unknown = 0,
FileURL = 1,
RemoteURL = 2,
OneBotBase64 = 3,
DataURL = 4,
Path = 5
}
export function checkUriType(uri: string): { type: FileUriType } {
if (uri.startsWith('base64://')) {
return { type: FileUriType.OneBotBase64 }
}
if (uri.startsWith('data:')) {
return { type: FileUriType.DataURL }
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
return { type: FileUriType.RemoteURL }
}
if (uri.startsWith('file://')) {
return { type: FileUriType.FileURL }
}
try {
if (fs.existsSync(uri)) return { type: FileUriType.Path }
} catch { }
return { type: FileUriType.Unknown }
}
interface FetchFileRes {
data: Buffer
url: string
}
async function fetchFile(url: string): Promise<FetchFileRes> {
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
'Host': new URL(url).hostname
}
const raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`)
return {
data: Buffer.from(await raw.arrayBuffer()),
url: raw.url
}
}
type Uri2LocalRes = { type Uri2LocalRes = {
success: boolean success: boolean
errMsg: string errMsg: string
fileName: string fileName: string
ext: string
path: string path: string
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> { export async function uri2local(uri: string, filename?: string): Promise<Uri2LocalRes> {
let res = { const { type } = checkUriType(uri)
success: false,
errMsg: '', if (type === FileUriType.FileURL) {
fileName: '', const filePath = fileURLToPath(uri)
ext: '', const fileName = path.basename(filePath)
path: '', return { success: true, errMsg: '', fileName, path: filePath, isLocal: true }
isLocal: false,
}
if (!fileName) {
fileName = randomUUID()
}
let filePath = path.join(TEMP_DIR, fileName)
let url: URL | null = null
try {
url = new URL(uri)
} catch (e: any) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
} }
// log("uri protocol", url.protocol, uri); if (type === FileUriType.Path) {
if (url.protocol == 'base64:') { const fileName = path.basename(uri)
// base64转成文件 return { success: true, errMsg: '', fileName, path: uri, isLocal: true }
let base64Data = uri.split('base64://')[1] }
if (type === FileUriType.RemoteURL) {
try { try {
const buffer = Buffer.from(base64Data, 'base64') const res = await fetchFile(uri)
await fsPromise.writeFile(filePath, buffer) const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
} catch (e: any) { if (match?.[1]) {
res.errMsg = `base64文件下载失败,` + e.toString() filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_')
return res
}
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件
let buffer: Buffer | null = null
try {
buffer = await httpDownload(uri)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name) {
fileName = pathInfo.name
if (pathInfo.ext) {
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_')
res.fileName = fileName
filePath = path.join(TEMP_DIR, randomUUID() + fileName)
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string
if (url.protocol === 'file:') {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === 'win32') {
filePath = pathname.slice(1)
} else { } else {
filePath = pathname filename ??= randomUUID()
} }
const filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data)
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e: any) {
const errMsg = `${uri}下载失败,` + e.toString()
return { success: false, errMsg, fileName: '', path: '', isLocal: false }
} }
}
res.isLocal = true if (type === FileUriType.OneBotBase64) {
filename ??= randomUUID()
const filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64')
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} }
// else{
// res.errMsg = `不支持的file协议,` + url.protocol if (type === FileUriType.DataURL) {
// return res // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
// } const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri)
// if (isGIF(filePath) && !res.isLocal) { if (capture) {
// await fs.rename(filePath, filePath + ".gif"); filename ??= randomUUID()
// filePath += ".gif"; const [, _type, base64] = capture
// } const filePath = path.join(TEMP_DIR, filename)
if (!res.isLocal && !res.ext) { await fsPromise.writeFile(filePath, base64, 'base64')
try { return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
const ext = (await fileType.fileTypeFromFile(filePath))?.ext
if (ext) {
log('获取文件类型', ext, filePath)
await fsPromise.rename(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
} }
} }
res.success = true
res.path = filePath return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false }
return res
} }
export async function copyFolder(sourcePath: string, destPath: string) { export async function copyFolder(sourcePath: string, destPath: string) {

View File

@@ -16,7 +16,7 @@ export class RequestUtil {
const redirectUrl = new URL(res.headers.location, url); const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => { RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies // 合并重定向过程中的cookies
log('redirectCookies', redirectCookies) //log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies }; cookies = { ...cookies, ...redirectCookies };
resolve(cookies); resolve(cookies);
}); });
@@ -33,7 +33,7 @@ export class RequestUtil {
}); });
if (res.headers['set-cookie']) { if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']); // console.log(res.headers['set-cookie']);
log('set-cookie', url, res.headers['set-cookie']); //log('set-cookie', url, res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => { res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('='); const parts = cookie.split(';')[0].split('=');
const key = parts[0]; const key = parts[0];

View File

@@ -14,9 +14,8 @@ 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, TEMP_DIR } from '../common/utils' import { DATA_DIR, getBuildVersion, TEMP_DIR } from '../common/utils'
import { import {
getGroupMember,
llonebotError, llonebotError,
setSelfInfo, setSelfInfo,
getSelfInfo, getSelfInfo,
@@ -28,7 +27,7 @@ import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook,
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { import {
FriendRequestNotify, FriendRequestNotify,
GroupNotifies, GroupNotify,
GroupNotifyTypes, GroupNotifyTypes,
RawMessage, RawMessage,
BuddyReqType, BuddyReqType,
@@ -150,7 +149,6 @@ function onLoad() {
const { debug, reportSelfMessage } = getConfigUtil().getConfig() const { debug, reportSelfMessage } = getConfigUtil().getConfig()
for (let message of msgList) { for (let message of msgList) {
// 过滤启动之前的消息 // 过滤启动之前的消息
// log('收到新消息', message);
if (parseInt(message.msgTime) < startTime / 1000) { if (parseInt(message.msgTime) < startTime / 1000) {
continue continue
} }
@@ -191,13 +189,6 @@ function onLoad() {
postOb11Event(privateEvent) postOb11Event(privateEvent)
} }
}) })
// OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
// log(message)
// if (friendAddEvent) {
// // log("post friend add event", friendAddEvent);
// postOb11Event(friendAddEvent)
// }
// })
} }
} }
@@ -245,6 +236,7 @@ function onLoad() {
log('report self message error: ', e.stack.toString()) log('report self message error: ', e.stack.toString())
} }
}) })
const processedGroupNotify: string[] = []
registerReceiveHook<{ registerReceiveHook<{
doubt: boolean doubt: boolean
oldestUnreadSeq: string oldestUnreadSeq: string
@@ -252,48 +244,43 @@ function onLoad() {
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => { }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) { if (payload.unreadCount) {
// log("开始获取群通知详情") // log("开始获取群通知详情")
let notify: GroupNotifies let notifies: GroupNotify[]
try { try {
notify = await NTQQGroupApi.getGroupNotifies() notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) { } catch (e) {
// log("获取群通知详情失败", e); // log("获取群通知详情失败", e);
return return
} }
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) { for (const notify of notifies) {
try { try {
notify.time = Date.now() notify.time = Date.now()
const notifyTime = parseInt(notify.seq) / 1000 const notifyTime = parseInt(notify.seq) / 1000
if (notifyTime < startTime) { const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notifyTime < startTime || processedGroupNotify.includes(flag)) {
continue continue
} }
log('收到群通知', notify) processedGroupNotify.push(flag)
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify) log('有成员退出通知', notify)
try { const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))!
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid) let operatorId = member1Uin
let operatorId = member1.uin let subType: GroupDecreaseSubType = 'leave'
let subType: GroupDecreaseSubType = 'leave' if (notify.user2.uid) {
if (notify.user2.uid) { // 是被踢的
// 是被踢的 const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid)
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid) if (member2Uin) {
operatorId = member2?.uin! operatorId = member2Uin
subType = 'kick'
} }
let groupDecreaseEvent = new OB11GroupDecreaseEvent( subType = 'kick'
parseInt(notify.group.groupCode),
parseInt(member1.uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
} catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
} }
const groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
} }
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求') log('有加群请求')
@@ -359,10 +346,13 @@ function onLoad() {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) { if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
continue continue
} }
if (+req.reqTime < startTime / 1000) {
continue
}
let userId = 0 let userId = 0
try { try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid) const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin!) userId = parseInt(requesterUin)
} catch (e) { } catch (e) {
log('获取加好友者QQ号失败', e) log('获取加好友者QQ号失败', e)
} }
@@ -381,7 +371,7 @@ function onLoad() {
let startTime = 0 // 毫秒 let startTime = 0 // 毫秒
async function start(uid: string, uin: string) { async function start(uid: string, uin: string) {
log('llonebot pid', process.pid) log('process pid', process.pid)
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
if (!config.enableLLOB) { if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动' llonebotError.otherError = 'LLOneBot 未启动'
@@ -393,10 +383,13 @@ function onLoad() {
} }
llonebotError.otherError = '' llonebotError.otherError = ''
startTime = Date.now() startTime = Date.now()
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! }) const WrapperSession = getSession()
if (WrapperSession) {
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession })
}
MessageUnique.init(uin) MessageUnique.init(uin)
log('start activate group member info') //log('start activate group member info')
// 下面两个会导致CPU占用过高QQ卡死 // 下面两个会导致CPU占用过高QQ卡死
// NTQQGroupApi.activateMemberInfoChange().then().catch(log) // NTQQGroupApi.activateMemberInfoChange().then().catch(log)
// NTQQGroupApi.activateMemberListChange().then().catch(log) // NTQQGroupApi.activateMemberListChange().then().catch(log)
@@ -418,6 +411,8 @@ function onLoad() {
log('LLOneBot start') log('LLOneBot start')
} }
const buildVersion = getBuildVersion()
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
const current = getSelfInfo() const current = getSelfInfo()
if (!current.uin) { if (!current.uin) {
@@ -427,7 +422,7 @@ function onLoad() {
nick: current.uin, nick: current.uin,
}) })
} }
if (current.uin && getSession()) { if (current.uin && (buildVersion >= 27187 || getSession())) {
clearInterval(intervalId) clearInterval(intervalId)
start(current.uid, current.uin) start(current.uid, current.uin)
} }
@@ -436,7 +431,7 @@ function onLoad() {
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (getSelfUid()) { if (window.id !== 2) {
return return
} }
mainWindow = window mainWindow = window

View File

@@ -1,4 +1,5 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { import {
CacheFileList, CacheFileList,
CacheFileListItem, CacheFileListItem,
@@ -16,7 +17,7 @@ import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log, TEMP_DIR } from '@/common/utils' import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey' import { rkeyManager } from '@/ntqqapi/helper/rkey'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
@@ -24,43 +25,23 @@ import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { NodeIKernelSearchService } from '@/ntqqapi/services' import { Time } from 'cosmokit'
export class NTQQFileApi { export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> { /** 27187 TODO */
static async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession() const session = getSession()
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer, return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
msgId, msgId,
elementId, elementId,
0, 0,
{ downSourceType: 1, triggerType: 1 }))?.urlResult?.domainUrl[0]?.url! { downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url
} }
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return fileTypeFromFile(filePath) return fileTypeFromFile(filePath)
} }
static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY,
args: [
{
fromPath: filePath,
toPath: destPath,
},
],
})
}
static async getFileSize(filePath: string) {
return await callNTQQApi<number>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_SIZE,
args: [filePath],
})
}
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath) const fileMd5 = await calculateFileMD5(filePath)
@@ -73,22 +54,43 @@ export class NTQQFileApi {
fileName += ext fileName += ext
} }
const session = getSession() const session = getSession()
const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({ let mediaPath: string
md5HexStr: fileMd5, if (session) {
fileName: fileName, mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
elementType: elementType, md5HexStr: fileMd5,
elementSubType, fileName: fileName,
thumbSize: 0, elementType: elementType,
needCreate: true, elementSubType,
downloadType: 1, thumbSize: 0,
file_uuid: '' needCreate: true,
}) downloadType: 1,
await fsPromise.copyFile(filePath, mediaPath!) file_uuid: ''
})
} else {
mediaPath = await invoke<string>({
methodName: NTMethod.MEDIA_FILE_PATH,
args: [
{
path_info: {
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
},
],
})
}
await fsPromise.copyFile(filePath, mediaPath)
const fileSize = (await fsPromise.stat(filePath)).size const fileSize = (await fsPromise.stat(filePath)).size
return { return {
md5: fileMd5, md5: fileMd5,
fileName, fileName,
path: mediaPath!, path: mediaPath,
fileSize, fileSize,
ext ext
} }
@@ -109,53 +111,79 @@ export class NTQQFileApi {
if (force) { if (force) {
try { try {
await fsPromise.unlink(sourcePath) await fsPromise.unlink(sourcePath)
} catch (e) { } catch { }
//
}
} else { } else {
return sourcePath return sourcePath
} }
} }
const data = await NTEventDispatch.CallNormalEvent< let filePath: string
( if (NTEventDispatch.initialised) {
params: { const data = await NTEventDispatch.CallNormalEvent<
fileModelId: string, (
downloadSourceType: number, params: {
triggerType: number, fileModelId: string,
msgId: string, downloadSourceType: number,
chatType: ChatType, triggerType: number,
peerUid: string, msgId: string,
elementId: string, chatType: ChatType,
thumbSize: number, peerUid: string,
downloadType: number, elementId: string,
filePath: string thumbSize: number,
}) => Promise<unknown>, downloadType: number,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void filePath: string
>( }) => Promise<unknown>,
'NodeIKernelMsgService/downloadRichMedia', (fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
'NodeIKernelMsgListener/onRichMediaDownloadComplete', >(
1, 'NodeIKernelMsgService/downloadRichMedia',
timeout, 'NodeIKernelMsgListener/onRichMediaDownloadComplete',
(arg: OnRichMediaDownloadCompleteParams) => { 1,
if (arg.msgId === msgId) { timeout,
return true (arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
} }
return false )
}, filePath = data[1].filePath
{ } else {
fileModelId: '0', const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>({
downloadSourceType: 0, methodName: NTMethod.DOWNLOAD_MEDIA,
triggerType: 1, args: [
msgId: msgId, {
chatType: chatType, getReq: {
peerUid: peerUid, fileModelId: '0',
elementId: elementId, downloadSourceType: 0,
thumbSize: 0, triggerType: 1,
downloadType: 1, msgId: msgId,
filePath: thumbPath chatType: chatType,
} peerUid: peerUid,
) elementId: elementId,
let filePath = data[1].filePath thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
],
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.msgId === msgId,
timeout
})
filePath = data.notifyInfo.filePath
}
if (filePath.startsWith('\\')) { if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath) filePath = path.join(downloadPath, filePath)
@@ -165,9 +193,9 @@ export class NTQQFileApi {
} }
static async getImageSize(filePath: string) { static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number; height: number }>({ return await invoke<{ width: number; height: number }>({
className: NTQQApiClass.FS_API, className: NTClass.FS_API,
methodName: NTQQApiMethod.IMAGE_SIZE, methodName: NTMethod.IMAGE_SIZE,
args: [filePath], args: [filePath],
}) })
} }
@@ -203,128 +231,12 @@ export class NTQQFileApi {
log('图片url获取失败', element) log('图片url获取失败', element)
return '' return ''
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/core/src/apis/file.ts#L149
static async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) {
let GroupData: any[] | undefined
let BuddyData: any[] | undefined
if (peer.chatType === ChatType.group) {
GroupData =
[{
groupCode: peer.peerUid,
isConf: false,
hasModifyConfGroupFace: true,
hasModifyConfGroupName: true,
groupName: 'LLOneBot.Cached',
remark: 'LLOneBot.Cached',
}];
} else if (peer.chatType === ChatType.friend) {
BuddyData = [{
category_name: 'LLOneBot.Cached',
peerUid: peer.peerUid,
peerUin: peer.peerUid,
remark: 'LLOneBot.Cached',
}]
} else {
return undefined
}
const session = getSession()
return session?.getSearchService().addSearchHistory({
type: 4,
contactList: [],
id: -1,
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: peer.chatType,
buddyChatInfo: BuddyData || [],
discussChatInfo: [],
groupChatInfo: GroupData || [],
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: msgId,
msgSeq: msgSeq,
msgTime: Math.floor(Date.now() / 1000).toString(),
senderUid: senderUid,
senderNick: 'LLOneBot.Cached',
senderRemark: 'LLOneBot.Cached',
senderCard: 'LLOneBot.Cached',
elemId: elemId,
elemType: elemType,
fileSize: fileSize,
filePath: '',
fileName: fileName,
hits: [{
start: 12,
end: 14,
}],
},
],
})
}
static async searchfile(keys: string[]) {
type EventType = NodeIKernelSearchService['searchFileWithKeywords']
interface OnListener {
searchId: string,
hasMore: boolean,
resultItems: {
chatType: ChatType,
buddyChatInfo: any[],
discussChatInfo: any[],
groupChatInfo:
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}[],
dataLineChatInfo: any[],
tmpChatInfo: any[],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: number,
fileSize: string,
filePath: string,
fileName: string,
hits:
{
start: number,
end: number
}[]
}[]
}
const Event = NTEventDispatch.createEventFunction<EventType>('NodeIKernelSearchService/searchFileWithKeywords')
let id = ''
const Listener = NTEventDispatch.RegisterListen<(params: OnListener) => void>
(
'NodeIKernelSearchListener/onSearchFileKeywordsResult',
1,
20000,
(params) => id !== '' && params.searchId == id,
)
id = await Event!(keys, 12)
const [ret] = await Listener
return ret
}
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) { static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE, methodName: NTMethod.CACHE_SET_SILENCE,
args: [ args: [
{ {
isSilent, isSilent,
@@ -335,21 +247,21 @@ export class NTQQFileCacheApi {
} }
static getCacheSessionPathList() { static getCacheSessionPathList() {
return callNTQQApi< return invoke<
{ {
key: string key: string
value: string value: string
}[] }[]
>({ >({
className: NTQQApiClass.OS_API, className: NTClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION, methodName: NTMethod.CACHE_PATH_SESSION,
}) })
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ return invoke<any>({
// TODO: 目前还不知道真正的返回值是什么 // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR, methodName: NTMethod.CACHE_CLEAR,
args: [ args: [
{ {
keys: cacheKeys, keys: cacheKeys,
@@ -360,8 +272,8 @@ export class NTQQFileCacheApi {
} }
static addCacheScannedPaths(pathMap: object = {}) { static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({ return invoke<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, methodName: NTMethod.CACHE_ADD_SCANNED_PATH,
args: [ args: [
{ {
pathMap: { ...pathMap }, pathMap: { ...pathMap },
@@ -372,35 +284,35 @@ export class NTQQFileCacheApi {
} }
static scanCache() { static scanCache() {
callNTQQApi<GeneralCallResult>({ invoke<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH, methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true, classNameIsRegister: true,
}).then() }).then()
return callNTQQApi<CacheScanResult>({ return invoke<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN, methodName: NTMethod.CACHE_SCAN,
args: [null, null], args: [null, null],
timeoutSecond: 300, timeout: 300 * Time.second,
}) })
} }
static getHotUpdateCachePath() { static getHotUpdateCachePath() {
return callNTQQApi<string>({ return invoke<string>({
className: NTQQApiClass.HOTUPDATE_API, className: NTClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE, methodName: NTMethod.CACHE_PATH_HOT_UPDATE,
}) })
} }
static getDesktopTmpPath() { static getDesktopTmpPath() {
return callNTQQApi<string>({ return invoke<string>({
className: NTQQApiClass.BUSINESS_API, className: NTClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP, methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP,
}) })
} }
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => { return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({ invoke<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET, methodName: NTMethod.CACHE_CHAT_GET,
args: [ args: [
{ {
chatType: type, chatType: type,
@@ -419,8 +331,8 @@ export class NTQQFileCacheApi {
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType } const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }
return callNTQQApi<CacheFileList>({ return invoke<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET, methodName: NTMethod.CACHE_FILE_GET,
args: [ args: [
{ {
fileType: fileType, fileType: fileType,
@@ -435,8 +347,8 @@ export class NTQQFileCacheApi {
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, methodName: NTMethod.CACHE_CHAT_CLEAR,
args: [ args: [
{ {
chats, chats,

View File

@@ -1,16 +1,16 @@
import { Friend, FriendV2 } from '../types' import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { CacheClassFuncAsyncExtend } from '@/common/utils/helper'
import { LimitedHashTable } from '@/common/utils/table' import { LimitedHashTable } from '@/common/utils/table'
import { pick } from 'cosmokit'
export class NTQQFriendApi { export class NTQQFriendApi {
/** >=26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
categroyName: string categroyName: string
@@ -18,12 +18,11 @@ export class NTQQFriendApi {
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>({ }>({
methodName: NTQQApiMethod.FRIENDS, methodName: NTMethod.FRIENDS,
args: [{ force_update: forced }, undefined], args: [{ force_update: forced }, undefined],
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false, afterFirstCmd: false,
}) })
// log('获取好友列表', data)
let _friends: Friend[] = [] let _friends: Friend[] = []
for (const fData of data.data) { for (const fData of data.data) {
_friends.push(...fData.buddyList) _friends.push(...fData.buddyList)
@@ -31,23 +30,6 @@ export class NTQQFriendApi {
return _friends return _friends
} }
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0,
},
},
null,
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) { static async handleFriendRequest(flag: string, accept: boolean) {
const data = flag.split('|') const data = flag.split('|')
if (data.length < 2) { if (data.length < 2) {
@@ -56,71 +38,150 @@ export class NTQQFriendApi {
const friendUid = data[0] const friendUid = data[0]
const reqTime = data[1] const reqTime = data[1]
const session = getSession() const session = getSession()
return session?.getBuddyService().approvalFriendRequest({ if (session) {
friendUid, return session.getBuddyService().approvalFriendRequest({
reqTime, friendUid,
accept reqTime,
}) accept
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_FRIEND_REQUEST,
args: [
{
approvalInfo: {
friendUid,
reqTime,
accept,
},
},
],
})
}
} }
static async getBuddyV2(refresh = false): Promise<FriendV2[]> { static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = []
const session = getSession() const session = getSession()
const buddyService = session?.getBuddyService() if (session) {
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) const uids: string[] = []
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!) const buddyService = session.getBuddyService()
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
) const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
return Array.from(data.values()) 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
} )
return Array.from(data.values())
@CacheClassFuncAsyncExtend(3600 * 1000, 'getBuddyIdMap', () => true) } else {
static async getBuddyIdMapCache(refresh = false): Promise<LimitedHashTable<string, string>> { const data = await invoke<{
return await NTQQFriendApi.getBuddyIdMap(refresh) buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
const categoryUids: Map<number, string[]> = new Map()
for (const item of data.buddyCategory) {
categoryUids.set(item.categoryId, item.buddyUids)
}
return Object.values(data.userSimpleInfos).filter(v => v.baseInfo && categoryUids.get(v.baseInfo.categoryId)?.includes(v.uid!))
}
} }
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> { static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const uids: string[] = []
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000) const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000)
const session = getSession() const session = getSession()
const buddyService = session?.getBuddyService() if (session) {
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) const uids: string[] = []
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!) const buddyService = session?.getBuddyService()
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
); const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
data.forEach((value, key) => { 'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
retMap.set(value.uin!, value.uid!) )
}) data.forEach((value, key) => {
//console.log('getBuddyIdMap', retMap.getValue) retMap.set(value.uin!, value.uid!)
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
for (const item of Object.values(data.userSimpleInfos)) {
retMap.set(item.uin!, item.uid!)
}
}
return retMap return retMap
} }
static async getBuddyV2ExWithCate(refresh = false) { static async getBuddyV2ExWithCate(refresh = false) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const session = getSession() const session = getSession()
const buddyService = session?.getBuddyService() if (session) {
const buddyListV2 = refresh ? (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data : (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data const uids: string[] = []
uids.push( const categoryMap: Map<string, any> = new Map()
...buddyListV2?.flatMap(item => { const buddyService = session.getBuddyService()
item.buddyUids.forEach(uid => { const buddyListV2 = (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName }) uids.push(
...buddyListV2?.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName })
})
return item.buddyUids
})!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map()
for (const item of data.buddyCategory) {
category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName']))
}
return Object.values(data.userSimpleInfos)
.filter(v => v.baseInfo && category.get(v.baseInfo.categoryId)?.buddyUids.includes(v.uid!))
.map(value => {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: category.get(value.baseInfo.categoryId)?.categroyName
}
}) })
return item.buddyUids }
})!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
} }
static async isBuddy(uid: string): Promise<boolean> { static async isBuddy(uid: string): Promise<boolean> {
const session = getSession() const session = getSession()
return session?.getBuddyService().isBuddy(uid)! if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke<boolean>({
methodName: 'nodeIKernelBuddyService/isBuddy',
args: [
{ uid },
null,
],
})
}
} }
} }

View File

@@ -1,98 +1,80 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindowApi, NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners' import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services'
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): Promise<Group[]> { static async getGroups(forced = false): Promise<Group[]> {
type ListenerType = NodeIKernelGroupListener['onGroupListUpdate'] if (NTEventDispatch.initialised) {
const [, , groupList] = await NTEventDispatch.CallNormalEvent type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']
<(force: boolean) => Promise<any>, ListenerType> const [, , groupList] = await NTEventDispatch.CallNormalEvent
( <(force: boolean) => Promise<any>, ListenerType>
'NodeIKernelGroupService/getGroupList', (
'NodeIKernelGroupListener/onGroupListUpdate', 'NodeIKernelGroupService/getGroupList',
1, 'NodeIKernelGroupListener/onGroupListUpdate',
5000, 1,
(updateType) => true, 5000,
forced () => true,
) forced
return groupList )
return groupList
} else {
const result = await invoke<{
updateType: number
groupList: Group[]
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getGroupList',
cbCmd: ReceiveCmdS.GROUPS_STORE,
afterFirstCmd: false,
})
return result.groupList
}
} }
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession() const session = getSession()
const groupService = session?.getGroupService() let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
const sceneId = groupService?.createMemberListScene(groupQQ, 'groupMemberList_MainWindow') if (session) {
const result = await groupService?.getNextMemberList(sceneId!, undefined, num) const groupService = session.getGroupService()
if (result?.errCode !== 0) { const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow')
throw ('获取群成员列表出错,' + result?.errMsg) result = await groupService.getNextMemberList(sceneId, undefined, num)
} else {
const sceneId = await invoke<string>({
methodName: NTMethod.GROUP_MEMBER_SCENE,
args: [
{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow',
},
],
})
result = await invoke<
ReturnType<NodeIKernelGroupService['getNextMemberList']>
>({
methodName: NTMethod.GROUP_MEMBERS,
args: [
{
sceneId,
num,
},
null,
],
})
}
if (result.errCode !== 0) {
throw ('获取群成员列表出错,' + result.errMsg)
} }
return result.result.infos return result.result.infos
} }
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() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: 14 }, null],
})
}
static async getGroupIgnoreNotifies() { static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies() await NTQQGroupApi.getSingleScreenNotifies(14)
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>( return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow, NTQQWindows.GroupNotifyFilterWindow,
[], [],
@@ -100,28 +82,91 @@ export class NTQQGroupApi {
) )
} }
static async getSingleScreenNotifies(num: number) {
if (NTEventDispatch.initialised) {
const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
<(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
(
'NodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
1,
5000,
() => true,
false,
'',
num,
)
return notifies
} else {
invoke({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
})
return (await invoke<GroupNotifies>({
methodName: NTMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: num }, null],
})).notifies
}
}
/** 27187 TODO */
static async delGroupFile(groupCode: string, files: string[]) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)
}
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|') const flagitem = flag.split('|')
const groupCode = flagitem[0] const groupCode = flagitem[0]
const seq = flagitem[1] const seq = flagitem[1]
const type = parseInt(flagitem[2]) const type = parseInt(flagitem[2])
const session = getSession() const session = getSession()
return session?.getGroupService().operateSysNotify( if (session) {
false, return session.getGroupService().operateSysNotify(
{ false,
'operateType': operateType, // 2 拒绝 {
'targetMsg': { 'operateType': operateType, // 2 拒绝
'seq': seq, // 通知序列号 'targetMsg': {
'type': type, 'seq': seq, // 通知序列号
'groupCode': groupCode, 'type': type,
'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 'groupCode': groupCode,
} 'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_GROUP_REQUEST,
args: [
{
doubt: false,
operateMsg: {
operateType,
targetMsg: {
seq,
type,
groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
},
},
},
null,
],
}) })
}
} }
static async quitGroup(groupQQ: string) { static async quitGroup(groupQQ: string) {
const session = getSession() const session = getSession()
return session?.getGroupService().quitGroup(groupQQ) if (session) {
return session.getGroupService().quitGroup(groupQQ)
} else {
return await invoke({
methodName: NTMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
}
} }
static async kickMember( static async kickMember(
@@ -131,37 +176,117 @@ export class NTQQGroupApi {
kickReason = '', kickReason = '',
) { ) {
const session = getSession() const session = getSession()
return session?.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason) if (session) {
return session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
} else {
return await invoke({
methodName: NTMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
},
],
})
}
} }
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 // timeStamp为秒数, 0为解除禁言
const session = getSession() const session = getSession()
return session?.getGroupService().setMemberShutUp(groupQQ, memList) if (session) {
return session.getGroupService().setMemberShutUp(groupQQ, memList)
} else {
return await invoke({
methodName: NTMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
},
],
})
}
} }
static async banGroup(groupQQ: string, shutUp: boolean) { static async banGroup(groupQQ: string, shutUp: boolean) {
const session = getSession() const session = getSession()
return session?.getGroupService().setGroupShutUp(groupQQ, shutUp) if (session) {
return session.getGroupService().setGroupShutUp(groupQQ, shutUp)
} else {
return await invoke({
methodName: NTMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
}
} }
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
const session = getSession() const session = getSession()
return session?.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName) if (session) {
return session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
} else {
return await invoke({
methodName: NTMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
}
} }
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
const session = getSession() const session = getSession()
return session?.getGroupService().modifyMemberRole(groupQQ, memberUid, role) if (session) {
return session.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
} else {
return await invoke({
methodName: NTMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
}
} }
static async setGroupName(groupQQ: string, groupName: string) { static async setGroupName(groupQQ: string, groupName: string) {
const session = getSession() const session = getSession()
return session?.getGroupService().modifyGroupName(groupQQ, groupName, false) if (session) {
return session.getGroupService().modifyGroupName(groupQQ, groupName, false)
} else {
return await invoke({
methodName: NTMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
}
} }
static async getGroupAtAllRemainCount(groupCode: string) { static async getGroupAtAllRemainCount(groupCode: string) {
return await callNTQQApi< return await invoke<
GeneralCallResult & { GeneralCallResult & {
atInfo: { atInfo: {
canAtAll: boolean canAtAll: boolean
@@ -172,7 +297,7 @@ export class NTQQGroupApi {
} }
} }
>({ >({
methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT, methodName: NTMethod.GROUP_AT_ALL_REMAIN_COUNT,
args: [ args: [
{ {
groupCode, groupCode,
@@ -182,17 +307,7 @@ export class NTQQGroupApi {
}) })
} }
static async getGroupRemainAtTimes(GroupCode: string) { /** 27187 TODO */
const session = getSession()
return session?.getGroupService().getGroupRemainAtTimes(GroupCode)!
}
// 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) { }
static async removeGroupEssence(GroupCode: string, msgId: string) { static async removeGroupEssence(GroupCode: string, msgId: string) {
const session = getSession() const session = getSession()
// 代码没测过 // 代码没测过
@@ -207,6 +322,7 @@ export class NTQQGroupApi {
return session?.getGroupService().removeGroupEssence(param) return session?.getGroupService().removeGroupEssence(param)
} }
/** 27187 TODO */
static async addGroupEssence(GroupCode: string, msgId: string) { static async addGroupEssence(GroupCode: string, msgId: string) {
const session = getSession() const session = getSession()
// 代码没测过 // 代码没测过

View File

@@ -1,36 +1,37 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult, TmpChatInfoApi } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { getSelfNick, getSelfUid } from '../../common/data' import { getSelfNick, getSelfUid } from '../../common/data'
import { getBuildVersion } from '../../common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000)
const random = Math.floor(Math.random() * Math.pow(2, 32))
const buffer = Buffer.alloc(8)
buffer.writeUInt32BE(timestamp, 0)
buffer.writeUInt32BE(random, 4)
const msgId = BigInt('0x' + buffer.toString('hex')).toString()
return msgId
}
export class NTQQMsgApi { export class NTQQMsgApi {
static async getTempChatInfo(chatType: ChatType2, peerUid: string) { static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession() const session = getSession()
return session?.getMsgService().getTempChatInfo(chatType, peerUid)! if (session) {
} return session.getMsgService().getTempChatInfo(chatType, peerUid)
} else {
static async prepareTempChat(toUserUid: string, GroupCode: string, nickname: string) { return await invoke<TmpChatInfoApi>({
//By Jadx/Ida Mlikiowa methodName: 'nodeIKernelMsgService/getTempChatInfo',
let TempGameSession = { args: [
nickname: '', {
gameAppId: '', chatType,
selfTinyId: '', peerUid,
peerRoleId: '', },
peerOpenId: '', null,
],
})
} }
const session = getSession()
return session?.getMsgService().prepareTempChat({
chatType: ChatType2.KCHATTYPETEMPC2CFROMGROUP,
peerUid: toUserUid,
peerNickname: nickname,
fromGroupCode: GroupCode,
sig: '',
selfPhone: '',
selfUid: getSelfUid(),
gameSession: TempGameSession
})
} }
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
@@ -39,28 +40,54 @@ export class NTQQMsgApi {
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString() emojiId = emojiId.toString()
const session = getSession() const session = getSession()
return session?.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set) if (session) {
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
} else {
return await invoke({
methodName: NTMethod.EMOJI_LIKE,
args: [
{
peer,
msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
}
} }
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
const session = getSession() const session = getSession()
return session?.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)! if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.GET_MULTI_MSG,
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
}
} }
static async activateChat(peer: Peer) { static async activateChat(peer: Peer) {
// await this.fetchRecentContact(); return await invoke<GeneralCallResult>({
// await sleep(500); methodName: NTMethod.ACTIVE_CHAT_PREVIEW,
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null], args: [{ peer, cnt: 20 }, null],
}) })
} }
static async activateChatAndGetHistory(peer: Peer) { static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact(); return await invoke<GeneralCallResult>({
// await sleep(500); methodName: NTMethod.ACTIVE_CHAT_HISTORY,
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样 // 参数似乎不是这样
args: [{ peer, cnt: 20 }, null], args: [{ peer, cnt: 20 }, null],
}) })
@@ -70,68 +97,120 @@ export class NTQQMsgApi {
if (!peer) throw new Error('peer is not allowed') if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed') if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession() const session = getSession()
//Mlikiowa 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得 if (session) {
return await session?.getMsgService().getMsgsByMsgId(peer, msgIds)! return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsByMsgId',
args: [
{
peer,
msgIds,
},
null,
],
})
}
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) { static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
const session = getSession() const session = getSession()
// 消息时间从旧到新 // 消息时间从旧到新
return session?.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)! if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.HISTORY_MSG,
args: [
{
peer,
msgId,
cnt: count,
queryOrder: isReverseOrder,
},
null,
],
})
}
} }
static async recallMsg(peer: Peer, msgIds: string[]) { static async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession() const session = getSession()
return await session?.getMsgService().recallMsg({ if (session) {
chatType: peer.chatType, return session.getMsgService().recallMsg({
peerUid: peer.peerUid chatType: peer.chatType,
}, msgIds) peerUid: peer.peerUid
}, msgIds)
} else {
return await invoke({
methodName: NTMethod.RECALL_MSG,
args: [
{
peer,
msgIds,
},
null,
],
})
}
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
function generateMsgId() { const msgId = generateMsgId()
const timestamp = Math.floor(Date.now() / 1000)
const random = Math.floor(Math.random() * Math.pow(2, 32))
const buffer = Buffer.alloc(8)
buffer.writeUInt32BE(timestamp, 0)
buffer.writeUInt32BE(random, 4)
const msgId = BigInt("0x" + buffer.toString('hex')).toString()
return msgId
}
// 此处有采用Hack方法 利用数据返回正确得到对应消息
// 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同
// 谨慎采用 目前测试暂无问题 Developer.Mlikiowa
let msgId: string
try {
msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime())
} catch (error) {
//if (!napCatCore.session.getMsgService()['generateMsgUniqueId'])
//兜底识别策略V2
msgId = generateMsgId()
}
peer.guildId = msgId peer.guildId = msgId
const data = await NTEventDispatch.CallNormalEvent< let msgList: RawMessage[]
(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>, if (NTEventDispatch.initialised) {
(msgList: RawMessage[]) => void const data = await NTEventDispatch.CallNormalEvent<
>( (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
'NodeIKernelMsgService/sendMsg', (msgList: RawMessage[]) => void
'NodeIKernelMsgListener/onMsgInfoListUpdate', >(
1, 'NodeIKernelMsgService/sendMsg',
timeout, 'NodeIKernelMsgListener/onMsgInfoListUpdate',
(msgRecords: RawMessage[]) => { 1,
for (let msgRecord of msgRecords) { timeout,
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) { (msgRecords: RawMessage[]) => {
return true for (const msgRecord of msgRecords) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
} }
} return false
return false },
}, '0',
'0', peer,
peer, msgElements,
msgElements, new Map()
new Map() )
) msgList = data[1]
const retMsg = data[1].find(msgRecord => { } else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/sendMsg',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
args: [
{
msgId: '0',
peer,
msgElements,
msgAttributeInfos: new Map()
},
null
],
timeout
})
msgList = data.msgList
}
const retMsg = msgList.find(msgRecord => {
if (msgRecord.guildId === msgId) { if (msgRecord.guildId === msgId) {
return true return true
} }
@@ -139,72 +218,25 @@ export class NTQQMsgApi {
return retMsg! return retMsg!
} }
static async sendMsgV2(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000)
const random = Math.floor(Math.random() * Math.pow(2, 32))
const buffer = Buffer.alloc(8)
buffer.writeUInt32BE(timestamp, 0)
buffer.writeUInt32BE(random, 4)
const msgId = BigInt('0x' + buffer.toString('hex')).toString()
return msgId
}
// 此处有采用Hack方法 利用数据返回正确得到对应消息
// 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同
// 谨慎采用 目前测试暂无问题 Developer.Mlikiowa
let msgId: string
try {
msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime())
} catch (error) {
//if (!napCatCore.session.getMsgService()['generateMsgUniqueId'])
//兜底识别策略V2
msgId = generateMsgId().toString()
}
let data = await NTEventDispatch.CallNormalEvent<
(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/sendMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
timeout,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.msgId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
msgId,
peer,
msgElements,
new Map()
)
const retMsg = data[1].find(msgRecord => {
if (msgRecord.msgId === msgId) {
return true
}
})
return retMsg!
}
static async getMsgUnique(chatType: number, time: string) {
const session = getSession()
if (getBuildVersion() >= 26702) {
return session?.getMsgService().generateMsgUniqueId(chatType, time)!
}
return session?.getMsgService().getMsgUniqueId(time)!
}
static async getServerTime() {
const session = getSession()
return session?.getMSFService().getServerTime()!
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession() const session = getSession()
return session?.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])! if (session) {
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} else {
return await invoke<GeneralCallResult>({
methodName: NTMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
}
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> { static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
@@ -213,29 +245,58 @@ export class NTQQMsgApi {
return { msgId: id, senderShowName } return { msgId: id, senderShowName }
}) })
const selfUid = getSelfUid() const selfUid = getSelfUid()
let data = await NTEventDispatch.CallNormalEvent< let msgList: RawMessage[]
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>, if (NTEventDispatch.initialised) {
(msgList: RawMessage[]) => void const data = await NTEventDispatch.CallNormalEvent<
>( (msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
'NodeIKernelMsgService/multiForwardMsgWithComment', (msgList: RawMessage[]) => void
'NodeIKernelMsgListener/onMsgInfoListUpdate', >(
1, 'NodeIKernelMsgService/multiForwardMsgWithComment',
5000, 'NodeIKernelMsgListener/onMsgInfoListUpdate',
(msgRecords: RawMessage[]) => { 1,
for (let msgRecord of msgRecords) { 5000,
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) { (msgRecords: RawMessage[]) => {
return true for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
} }
} return false
return false },
}, msgInfos,
msgInfos, srcPeer,
srcPeer, destPeer,
destPeer, [],
[], new Map()
new Map() )
) msgList = data[1]
for (let msg of data[1]) { } else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/multiForwardMsgWithComment',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
},
args: [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
msgList = data.msgList
}
for (const msg of msgList) {
const arkElement = msg.elements.find(ele => ele.arkElement) const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) { if (!arkElement) {
continue continue
@@ -251,26 +312,29 @@ export class NTQQMsgApi {
throw new Error('转发消息超时') throw new Error('转发消息超时')
} }
static async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
})
return ret!
}
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
const session = getSession() const session = getSession()
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)! if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsBySeqAndCount',
args: [
{
peer,
cnt: count,
msgSeq: seq,
queryOrder: desc
},
null,
],
})
}
} }
/** 27187 TODO */
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) { static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession() const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
@@ -283,11 +347,26 @@ export class NTQQMsgApi {
isIncludeCurrent: true, isIncludeCurrent: true,
pageLimit: count, pageLimit: count,
}) })
return ret! return ret
} }
static async getSingleMsg(peer: Peer, seq: string) { static async getSingleMsg(peer: Peer, seq: string) {
const session = getSession() const session = getSession()
return await session?.getMsgService().getSingleMsg(peer, seq)! if (session) {
return await session.getMsgService().getSingleMsg(peer, seq)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getSingleMsg',
args: [
{
peer,
msgSeq: seq,
},
null,
],
})
}
} }
} }

View File

@@ -1,124 +1,119 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types' import { GeneralCallResult } from '../services'
import { ReceiveCmdS } from '../hook' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import { friends, groupMembers, getSelfUin } from '@/common/data' import { friends, groupMembers, getSelfUin } from '@/common/data'
import { CacheClassFuncAsync, log, getBuildVersion } from '@/common/utils' import { CacheClassFuncAsync, getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services'
import { NodeIKernelProfileListener } from '../listeners' import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { NTQQFriendApi } from './friend' import { NTQQFriendApi } from './friend'
import { Time } from 'cosmokit'
export class NTQQUserApi { export class NTQQUserApi {
static async setQQAvatar(filePath: string) { static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({ return await invoke<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR, methodName: NTMethod.SET_QQ_AVATAR,
args: [ args: [
{ {
path: filePath, path: filePath,
}, },
null, null,
], ],
timeoutSecond: 10, // 10秒不一定够 timeout: 10 * Time.second, // 10秒不一定够
}) })
} }
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO,
timeoutSecond: 2,
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{ force: true, uids: [uid] }, undefined],
cbCmd: ReceiveCmdS.USER_INFO,
})
return result.profiles.get(uid)
}
/** 26702 */
static async fetchUserDetailInfo(uid: string) { static async fetchUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo'] let info: UserDetailInfoListenerArg
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged'] if (NTEventDispatch.initialised) {
const [_retData, profile] = await NTEventDispatch.CallNormalEvent type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
<EventService, EventListener> type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
( const [_retData, profile] = await NTEventDispatch.CallNormalEvent
'NodeIKernelProfileService/fetchUserDetailInfo', <EventService, EventListener>
'NodeIKernelProfileListener/onUserDetailInfoChanged', (
1, 'NodeIKernelProfileService/fetchUserDetailInfo',
5000, 'NodeIKernelProfileListener/onUserDetailInfoChanged',
(profile) => profile.uid === uid, 1,
'BuddyProfileStore', 5000,
[uid], (profile) => profile.uid === uid,
UserDetailSource.KSERVER, 'BuddyProfileStore',
[ProfileBizType.KALL] [uid],
) UserDetailSource.KSERVER,
const RetUser: User = { [ProfileBizType.KALL]
...profile.simpleInfo.coreInfo, )
...profile.simpleInfo.status, info = profile
...profile.simpleInfo.vasInfo, } else {
...profile.commonExt, const result = await invoke<{ info: UserDetailInfoListenerArg }>({
...profile.simpleInfo.baseInfo, methodName: 'nodeIKernelProfileService/fetchUserDetailInfo',
qqLevel: profile.commonExt.qqLevel, cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
args: [
{
callFrom: 'BuddyProfileStore',
uid: [uid],
source: UserDetailSource.KSERVER,
bizList: [ProfileBizType.KALL]
},
null
],
})
info = result.info
}
const ret: User = {
...info.simpleInfo.coreInfo,
...info.simpleInfo.status,
...info.simpleInfo.vasInfo,
...info.commonExt,
...info.simpleInfo.baseInfo,
qqLevel: info.commonExt?.qqLevel,
pendantId: '' pendantId: ''
} }
return RetUser return ret
} }
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return NTQQUserApi.fetchUserDetailInfo(uid) return NTQQUserApi.fetchUserDetailInfo(uid)
} }
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] if (NTEventDispatch.initialised) {
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'] type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']
<EventService, EventListener> const [_retData, profile] = await NTEventDispatch.CallNormalEvent
( <EventService, EventListener>
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo', (
'NodeIKernelProfileListener/onProfileDetailInfoChanged', 'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
2, 'NodeIKernelProfileListener/onProfileDetailInfoChanged',
5000, 2,
(profile) => profile.uid === uid, 5000,
uid, (profile) => profile.uid === uid,
[0] uid,
) [0]
return profile )
} return profile
} else {
// return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey=' const result = await invoke<{ info: User }>({
static async getCookieWithoutSkey() { methodName: 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
return await callNTQQApi<string>({ cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
className: NTQQApiClass.GROUP_HOME_WORK, afterFirstCmd: false,
methodName: NTQQApiMethod.UPDATE_SKEY, cmdCB: (payload) => payload.info.uid === uid,
args: [ args: [
{ {
domain: 'qun.qq.com', uid,
}, bizList: [0]
], },
}) null,
} ],
})
static async getQzoneCookies() { return result.info
const uin = getSelfUin()
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + (await NTQQUserApi.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + 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 NTQQUserApi.getClientKey() const clientKeyData = await NTQQUserApi.forceFetchClientKey()
if (clientKeyData.result !== 0) { if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败') throw new Error('获取clientKey失败')
} }
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin() const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin()
@@ -129,9 +124,12 @@ export class NTQQUserApi {
@CacheClassFuncAsync(1800 * 1000) @CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) { static async getCookies(domain: string) {
const ClientKeyData = await NTQQUserApi.forceFetchClientKey() const clientKeyData = await NTQQUserApi.forceFetchClientKey()
if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败')
}
const uin = getSelfUin() const uin = getSelfUin()
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27' const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27'
const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl) const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
return cookies return cookies
} }
@@ -148,28 +146,31 @@ export class NTQQUserApi {
return (hash & 0x7fffffff).toString() return (hash & 0x7fffffff).toString()
} }
static async getPSkey(domains: string[]): Promise<Map<string, string>> { static async like(uid: string, count = 1) {
const session = getSession() const session = getSession()
const res = await session?.getTipOffService().getPskey(domains, true) if (session) {
if (res?.result !== 0) { return session.getProfileLikeService().setBuddyProfileLike({
throw new Error(`获取Pskey失败: ${res?.errMsg}`) friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})
} else {
return await invoke<GeneralCallResult & { succCounts: number }>({
methodName: 'nodeIKernelProfileLikeService/setBuddyProfileLike',
args: [
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null,
],
})
} }
return res.domainPskeyMap
}
static async getClientKey() {
const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')!
}
static async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> {
const session = getSession()
return session?.getProfileLikeService().setBuddyProfileLike({
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})!
} }
static async getUidByUinV1(Uin: string) { static async getUidByUinV1(Uin: string) {
@@ -203,23 +204,46 @@ export class NTQQUserApi {
return uid return uid
} }
static async getUidByUinV2(Uin: string) { static async getUidByUinV2(uin: string) {
const session = getSession() const session = getSession()
let uid = (await session?.getProfileService().getUidByUin('FriendsServiceImpl', [Uin]))?.get(Uin) if (session) {
if (uid) return uid let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin)
uid = (await session?.getGroupService().getUidByUins([Uin]))?.uids.get(Uin) if (uid) return uid
if (uid) return uid uid = (await session.getProfileService().getUidByUin('FriendsServiceImpl', [uin])).get(uin)
uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin) if (uid) return uid
if (uid) return uid uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin)
console.log((await NTQQFriendApi.getBuddyIdMapCache(true))) if (uid) return uid
uid = (await NTQQFriendApi.getBuddyIdMapCache(true)).getValue(Uin)//从Buddy缓存获取Uid } else {
if (uid) return uid let uid = (await invoke<{ uids: Map<string, string> }>({
uid = (await NTQQFriendApi.getBuddyIdMap(true)).getValue(Uin) methodName: 'nodeIKernelGroupService/getUidByUins',
if (uid) return uid args: [
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(Uin)).detail.uid//从QQ Native 特殊转换 { uin: [uin] },
if (unveifyUid.indexOf('*') == -1) uid = unveifyUid null,
//if (uid) return uid ],
return uid })).uids.get(uin)
if (uid) return uid
uid = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUidByUin',
args: [
{
callFrom: 'FriendsServiceImpl',
uin: [uin],
},
null,
],
})).get(uin)
if (uid) return uid
uid = (await invoke<{ uidInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUid',
args: [
{ uin: [uin] },
null,
],
})).uidInfo.get(uin)
if (uid) return uid
}
const unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
} }
static async getUidByUin(Uin: string) { static async getUidByUin(Uin: string) {
@@ -229,14 +253,25 @@ export class NTQQUserApi {
return await NTQQUserApi.getUidByUinV1(Uin) return await NTQQUserApi.getUidByUinV1(Uin)
} }
static async getUserDetailInfoByUinV2(Uin: string) { static async getUserDetailInfoByUinV2(uin: string) {
return await NTEventDispatch.CallNoListenerEvent if (NTEventDispatch.initialised) {
<(Uin: string) => Promise<UserDetailInfoByUinV2>>( return await NTEventDispatch.CallNoListenerEvent
'NodeIKernelProfileService/getUserDetailInfoByUin', <(Uin: string) => Promise<UserDetailInfoByUinV2>>(
5000, 'NodeIKernelProfileService/getUserDetailInfoByUin',
Uin 5000,
) uin
)
} else {
return await invoke<UserDetailInfoByUinV2>({
methodName: 'nodeIKernelProfileService/getUserDetailInfoByUin',
args: [
{ uin },
null,
],
})
}
} }
static async getUserDetailInfoByUin(Uin: string) { static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>( <(Uin: string) => Promise<UserDetailInfoByUin>>(
@@ -268,32 +303,68 @@ export class NTQQUserApi {
return uin return uin
} }
static async getUinByUidV2(Uid: string) { static async getUinByUidV2(uid: string) {
const session = getSession() const session = getSession()
let uin = (await session?.getProfileService().getUinByUid('FriendsServiceImpl', [Uid]))?.get(Uid) if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
if (uin) return uin
uin = (await session.getProfileService().getUinByUid('FriendsServiceImpl', [uid])).get(uid)
if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
return uin
} else {
let uin = (await invoke<{ uins: Map<string, string> }>({
methodName: 'nodeIKernelGroupService/getUinByUids',
args: [
{ uid: [uid] },
null,
],
})).uins.get(uid)
if (uin) return uin
uin = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUinByUid',
args: [
{
callFrom: 'FriendsServiceImpl',
uid: [uid],
},
null,
],
})).get(uid)
if (uin) return uin
uin = (await invoke<{ uinInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUin',
args: [
{ uid: [uid] },
null,
],
})).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(uid)
if (uin) return uin if (uin) return uin
uin = (await session?.getGroupService().getUinByUids([Uid]))?.uins.get(Uid) uin = (await NTQQUserApi.getUserDetailInfo(uid)).uin //从QQ Native 转换
if (uin) return uin
uin = (await session?.getUixConvertService().getUin([Uid]))?.uinInfo.get(Uid)
if (uin) return uin
uin = (await NTQQFriendApi.getBuddyIdMapCache(true)).getKey(Uid) //从Buddy缓存获取Uin
if (uin) return uin
uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(Uid)
if (uin) return uin
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
return uin
} }
static async getUinByUid(Uid: string) { static async getUinByUid(Uid: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUinByUidV2(Uid) return (await NTQQUserApi.getUinByUidV2(Uid))!
} }
return await NTQQUserApi.getUinByUidV1(Uid) return await NTQQUserApi.getUinByUidV1(Uid)
} }
@CacheClassFuncAsync(3600 * 1000, 'ClientKey')
static async forceFetchClientKey() { static async forceFetchClientKey() {
const session = getSession() const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')! if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke<forceFetchClientKeyRetType>({
methodName: 'nodeIKernelTicketService/forceFetchClientKey',
args: [{
domain: ''
}, null],
})
}
} }
} }

View File

@@ -2,7 +2,6 @@ import { getSelfUin } from '@/common/data'
import { log } from '@/common/utils/log' import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user' import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { CacheClassFuncAsync } from '@/common/utils/helper'
export enum WebHonorType { export enum WebHonorType {
ALL = 'all', ALL = 'all',
@@ -138,101 +137,45 @@ export class WebApi {
return ret return ret
} }
@CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members')
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
//logDebug('webapi 获取群成员', GroupCode) const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>() const cookieObject = await NTQQUserApi.getCookies('qun.qq.com')
try { const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com') const retList: Promise<WebApiGroupMemberRet>[] = []
const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') const params = new URLSearchParams({
const Bkn = WebApi.genBkn(CookiesObject.skey) st: '0',
const retList: Promise<WebApiGroupMemberRet>[] = [] end: '40',
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); sort: '1',
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { gc: GroupCode,
return [] bkn: WebApi.genBkn(cookieObject.skey)
} else { })
for (const key in fastRet.mems) { const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
MemberData.push(fastRet.mems[key]) if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
} return []
} else {
for (const member of fastRet.mems) {
memberData.push(member)
} }
//初始化获取PageNum }
const PageNum = Math.ceil(fastRet.count / 40) const pageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求 //遍历批量请求
for (let i = 2; i <= PageNum; i++) { for (let i = 2; i <= pageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); params.set('st', String((i - 1) * 40))
retList.push(ret) params.set('end', String(i * 40))
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
retList.push(ret)
}
//批量等待
for (let i = 1; i <= pageNum; i++) {
const ret = await (retList[i])
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue
} }
//批量等待 for (const member of ret.mems) {
for (let i = 1; i <= PageNum; i++) { memberData.push(member)
const ret = await (retList[i])
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key])
}
} }
} catch {
return MemberData
}
return MemberData
}
// public static async addGroupDigest(groupCode: string, msgSeq: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`;
// const res = await this.request(url);
// return await res.json();
// }
// public async getGroupDigest(groupCode: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`;
// const res = await this.request(url);
// return await res.json();
// }
static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']
const _Skey = await NTQQUserApi.getSkey()
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin()
let ret: any = undefined
//console.log(CookieValue)
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined
}
const Bkn = WebApi.genBkn(_Skey)
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}'
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn
try {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue })
return ret
} catch (e) {
return undefined
}
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']
const _Skey = await NTQQUserApi.getSkey()
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin()
let ret: WebApiGroupNoticeRet | undefined = undefined
//console.log(CookieValue)
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined
}
const Bkn = WebApi.genBkn(_Skey)
const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20'
try {
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue })
if (ret?.ec !== 0) {
return undefined
}
return ret
} catch (e) {
return undefined
} }
return memberData
} }
static genBkn(sKey: string) { static genBkn(sKey: string) {
@@ -254,7 +197,7 @@ export class WebApi {
let res = ''; let res = '';
let resJson; let resJson;
try { try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue }); res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr });
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/); const match = res.match(/window\.__INITIAL_STATE__=(.*?);/);
if (match) { if (match) {
resJson = JSON.parse(match[1].trim()); resJson = JSON.parse(match[1].trim());
@@ -271,7 +214,8 @@ export class WebApi {
} }
let HonorInfo: any = { group_id: groupCode }; let HonorInfo: any = { group_id: groupCode };
const CookieValue = (await NTQQUserApi.getCookies('qun.qq.com')).cookies; const cookieObject = await NTQQUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try { try {

View File

@@ -1,4 +1,5 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { ReceiveCmd } from '../hook' import { ReceiveCmd } from '../hook'
import { BrowserWindow } from 'electron' import { BrowserWindow } from 'electron'
@@ -27,12 +28,12 @@ export class NTQQWindowApi {
static async openWindow<R = GeneralCallResult>( static async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: any[], args: any[],
cbCmd: ReceiveCmd | null = null, cbCmd: ReceiveCmd | undefined,
autoCloseSeconds: number = 2, autoCloseSeconds: number = 2,
) { ) {
const result = await callNTQQApi<R>({ const result = await invoke<R>({
className: NTQQApiClass.WINDOW_API, className: NTClass.WINDOW_API,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW, methodName: NTMethod.OPEN_EXTRA_WINDOW,
cbCmd, cbCmd,
afterFirstCmd: false, afterFirstCmd: false,
args: [ntQQWindow.windowName, ...args], args: [ntQQWindow.windowName, ...args],

View File

@@ -21,7 +21,7 @@ import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils' import { isNull } from '../common/utils'
import faceConfig from './face_config.json' import faceConfig from './helper/face_config.json'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName export const mFaceCache = new Map<string, string>() // emojiId -> faceName

View File

@@ -1,5 +1,3 @@
//远端rkey获取
import { log } from '@/common/utils' import { log } from '@/common/utils'
interface ServerRkeyData { interface ServerRkeyData {

View File

@@ -1,30 +1,23 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTClass, NTMethod } from './ntcall'
import { NTQQMsgApi } from './api/msg' import { NTQQMsgApi } from './api/msg'
import { import {
CategoryFriend, CategoryFriend,
ChatType, ChatType,
FriendV2,
Group,
GroupMember, GroupMember,
GroupMemberRole, GroupMemberRole,
RawMessage, RawMessage,
SimpleInfo, User, SimpleInfo, User,
} from './types' } from './types'
import { import {
deleteGroup,
friends, friends,
getFriend, getFriend,
getGroupMember, getGroupMember,
groups,
getSelfUin,
setSelfInfo setSelfInfo
} from '@/common/data' } from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config' import { getConfigUtil } from '@/common/config'
import fs from 'node:fs' import fs from 'node:fs'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique' import { MessageUnique } from '../common/utils/MessageUnique'
@@ -56,83 +49,74 @@ export let ReceiveCmdS = {
CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate', SKEY_UPDATE: 'onSkeyUpdate',
} } as const
export type ReceiveCmd = (typeof ReceiveCmdS)[keyof typeof ReceiveCmdS] export type ReceiveCmd = string
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> { interface NTQQApiReturnData<Payload = unknown> extends Array<any> {
0: { 0: {
type: 'request' type: 'request'
eventName: NTQQApiClass eventName: NTClass
callbackId?: string callbackId?: string
} }
1: { 1: {
cmdName: ReceiveCmd cmdName: ReceiveCmd
cmdType: 'event' cmdType: 'event'
payload: PayloadType payload: Payload
}[] }[]
} }
let receiveHooks: Array<{ const logHook = false
const receiveHooks: Array<{
method: ReceiveCmd[] method: ReceiveCmd[]
hookFunc: (payload: any) => void | Promise<void> hookFunc: (payload: any) => void | Promise<void>
id: string id: string
}> = [] }> = []
let callHooks: Array<{ const callHooks: Array<{
method: NTQQApiMethod[] method: NTMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow) { export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send const originalSend = window.webContents.send
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
// console.log("hookNTQQApiReceive", channel, args)
let isLogger = false
try { try {
isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') const isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) { } if (logHook && !isLogger) {
if (!isLogger) { log(`received ntqq api message: ${channel}`, args)
try {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
} catch (e) {
log('hook log error', e, args)
} }
} } catch { }
try { if (args?.[1] instanceof Array) {
if (args?.[1] instanceof Array) { for (const receiveData of args?.[1]) {
for (let receiveData of args?.[1]) { const ntQQApiMethodName = receiveData.cmdName
const ntQQApiMethodName = receiveData.cmdName // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) for (const hook of receiveHooks) {
for (let hook of receiveHooks) { if (hook.method.includes(ntQQApiMethodName)) {
if (hook.method.includes(ntQQApiMethodName)) { new Promise((resolve, reject) => {
new Promise((resolve, reject) => { try {
try { hook.hookFunc(receiveData.payload)
let _ = hook.hookFunc(receiveData.payload) } catch (e: any) {
if (hook.hookFunc.constructor.name === 'AsyncFunction') { log('hook error', ntQQApiMethodName, e.stack.toString())
; (_ as Promise<void>).then() }
} resolve(undefined)
} catch (e: any) { }).then()
log('hook error', ntQQApiMethodName, e.stack.toString())
}
}).then()
}
} }
} }
} }
if (args[0]?.callbackId) { }
// log("hookApiCallback", hookApiCallbacks, args) if (args[0]?.callbackId) {
const callbackId = args[0].callbackId // log("hookApiCallback", hookApiCallbacks, args)
if (hookApiCallbacks[callbackId]) { const callbackId = args[0].callbackId
// log("callback found") if (hookApiCallbacks[callbackId]) {
new Promise((resolve, reject) => { // log("callback found")
hookApiCallbacks[callbackId](args[1]) new Promise((resolve, reject) => {
}).then() hookApiCallbacks[callbackId](args[1])
delete hookApiCallbacks[callbackId] resolve(undefined)
} }).then()
delete hookApiCallbacks[callbackId]
} }
} catch (e: any) {
log('hookNTQQApiReceive error', e.stack.toString(), args)
} }
originalSend.call(window.webContents, channel, ...args) originalSend.call(window.webContents, channel, ...args)
} }
@@ -153,23 +137,21 @@ export function hookNTQQApiCall(window: BrowserWindow) {
} catch (e) { } } catch (e) { }
if (!isLogger) { if (!isLogger) {
try { try {
HOOK_LOG && log('call NTQQ api', thisArg, args) logHook && log('call NTQQ api', thisArg, args)
} catch (e) { } } catch (e) { }
try { try {
const _args: unknown[] = args[3][1] const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod const cmdName: NTMethod = _args[0] as NTMethod
const callParams = _args.slice(1) const callParams = _args.slice(1)
callHooks.forEach((hook) => { callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) { if (hook.method.includes(cmdName)) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
let _ = hook.hookFunc(callParams) hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') { } catch (e: any) {
(_ as Promise<void>).then()
}
} catch (e) {
log('hook call error', e, _args) log('hook call error', e, _args)
} }
resolve(undefined)
}).then() }).then()
} }
}) })
@@ -188,16 +170,16 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
// console.log(args); // console.log(args);
HOOK_LOG && log('call NTQQ invoke api', thisArg, args) //HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], { args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) { apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs) sendtarget.apply(sendthisArg, sendargs)
}, },
}) })
let ret = target.apply(thisArg, args) let ret = target.apply(thisArg, args)
try { /*try {
HOOK_LOG && log('call NTQQ invoke api return', ret) HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) { } } catch (e) { }*/
return ret return ret
}, },
}) })
@@ -225,7 +207,7 @@ export function registerReceiveHook<PayloadType>(
} }
export function registerCallHook( export function registerCallHook(
method: NTQQApiMethod | NTQQApiMethod[], method: NTMethod | NTMethod[],
hookFunc: (callParams: unknown[]) => void | Promise<void>, hookFunc: (callParams: unknown[]) => void | Promise<void>,
): void { ): void {
if (!Array.isArray(method)) { if (!Array.isArray(method)) {
@@ -242,9 +224,9 @@ export function removeReceiveHook(id: string) {
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1)
} }
let activatedGroups: string[] = [] //let activatedGroups: string[] = []
async function updateGroups(_groups: Group[], needUpdate: boolean = true) { /*async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) { for (let group of _groups) {
log('update group', group.groupCode) log('update group', group.groupCode)
if (group.privilegeFlag === 0) { if (group.privilegeFlag === 0) {
@@ -269,9 +251,9 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
} }
} }
} }
} }*/
async function processGroupEvent(payload: { groupList: Group[] }) { /*async function processGroupEvent(payload: { groupList: Group[] }) {
try { try {
const newGroupList = payload.groupList const newGroupList = payload.groupList
for (const group of newGroupList) { for (const group of newGroupList) {
@@ -322,12 +304,12 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString()) log('更新群信息错误', e.stack.toString())
} }
} }*/
export async function startHook() { export async function startHook() {
// 群列表变动 // 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { /*registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
@@ -350,7 +332,7 @@ export async function startHook() {
processGroupEvent(payload).then() processGroupEvent(payload).then()
} }
} }
}) })*/
registerReceiveHook<{ registerReceiveHook<{
groupCode: string groupCode: string
@@ -402,7 +384,7 @@ export async function startHook() {
}>(ReceiveCmdS.FRIENDS, (payload) => { }>(ReceiveCmdS.FRIENDS, (payload) => {
// log("onBuddyListChange", payload) // log("onBuddyListChange", payload)
// let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = [] // let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
type V2data = {userSimpleInfos: Map<string, SimpleInfo>} type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
let friendList: User[] = []; let friendList: User[] = [];
if ((payload as any).userSimpleInfos) { if ((payload as any).userSimpleInfos) {
// friendListV2 = payload as any // friendListV2 = payload as any
@@ -412,12 +394,12 @@ export async function startHook() {
} }
}) })
} }
else{ else {
for (const fData of payload.data) { for (const fData of payload.data) {
friendList.push(...fData.buddyList) friendList.push(...fData.buddyList)
} }
} }
log('好友列表变动', friendList) log('好友列表变动', friendList.length)
for (let friend of friendList) { for (let friend of friendList) {
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)
@@ -517,7 +499,7 @@ export async function startHook() {
} }
}) })
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => { registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid) log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend let chatType = ChatType.friend

View File

@@ -1,10 +1,10 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { HOOK_LOG } from '../common/config'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { GeneralCallResult } from './services'
export enum NTQQApiClass { export enum NTClass {
NT_API = 'ns-ntApi', NT_API = 'ns-ntApi',
FS_API = 'ns-FsApi', FS_API = 'ns-FsApi',
OS_API = 'ns-OsApi', OS_API = 'ns-OsApi',
@@ -15,10 +15,10 @@ export enum NTQQApiClass {
SKEY_API = 'ns-SkeyApi', SKEY_API = 'ns-SkeyApi',
GROUP_HOME_WORK = 'ns-GroupHomeWork', GROUP_HOME_WORK = 'ns-GroupHomeWork',
GROUP_ESSENCE = 'ns-GroupEssence', GROUP_ESSENCE = 'ns-GroupEssence',
NODE_STORE_API = 'ns-NodeStoreApi'
} }
export enum NTQQApiMethod { export enum NTMethod {
TEST = 'NodeIKernelTipOffService/getPskey',
RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact', RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息 ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息 ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
@@ -57,7 +57,6 @@ export enum NTQQApiMethod {
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify', HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup', QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes', GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest', HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
KICK_MEMBER = 'nodeIKernelGroupService/kickMember', KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp', MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
@@ -87,59 +86,41 @@ export enum NTQQApiMethod {
OPEN_EXTRA_WINDOW = 'openExternalWindow', OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_PSKEY = 'nodeIKernelTipOffService/getPskey',
UPDATE_SKEY = 'updatePskey',
FETCH_UNITED_COMMEND_CONFIG = 'nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', // 发包需要调用的
} }
enum NTQQApiChannel { export enum NTChannel {
IPC_UP_2 = 'IPC_UP_2', IPC_UP_2 = 'IPC_UP_2',
IPC_UP_3 = 'IPC_UP_3', IPC_UP_3 = 'IPC_UP_3',
IPC_UP_1 = 'IPC_UP_1', IPC_UP_1 = 'IPC_UP_1',
} }
interface NTQQApiParams { interface InvokeParams<ReturnType> {
methodName: NTQQApiMethod | string methodName: string
className?: NTQQApiClass className?: NTClass
channel?: NTQQApiChannel channel?: NTChannel
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[] args?: unknown[]
cbCmd?: ReceiveCmd | ReceiveCmd[] | null cbCmd?: string | string[]
cmdCB?: (payload: any) => boolean cmdCB?: (payload: ReturnType) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number timeout?: number
} }
export function callNTQQApi<ReturnType>(params: NTQQApiParams) { export function invoke<ReturnType>(params: InvokeParams<ReturnType>) {
let { const className = params.className ?? NTClass.NT_API
className, const channel = params.channel ?? NTChannel.IPC_UP_2
methodName, const timeout = params.timeout ?? 5000
channel, const afterFirstCmd = params.afterFirstCmd ?? true
args,
cbCmd,
timeoutSecond: timeout,
classNameIsRegister,
cmdCB,
afterFirstCmd,
} = params
className = className ?? NTQQApiClass.NT_API
channel = channel ?? NTQQApiChannel.IPC_UP_2
args = args ?? []
timeout = timeout ?? 5
afterFirstCmd = afterFirstCmd ?? true
const uuid = randomUUID() const uuid = randomUUID()
HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid) let eventName = className + '-' + channel[channel.length - 1]
if (params.classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [params.methodName, ...(params.args ?? [])]
//log('callNTQQApi', channel, eventName, apiArgs, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000
let success = false let success = false
let eventName = className + '-' + channel[channel.length - 1] if (!params.cbCmd) {
if (classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [methodName, ...args]
if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别 // QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true success = true
@@ -149,10 +130,10 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
else { else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<ReturnType>(params.cbCmd!, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB); // log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!cmdCB) { if (!!params.cmdCB) {
if (cmdCB(payload)) { if (params.cmdCB(payload)) {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true success = true
resolve(payload) resolve(payload)
@@ -167,23 +148,22 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
} }
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result) if (result?.result === 0 || result === undefined) {
if (result?.result == 0 || result === undefined) { log(`${params.methodName} callback`, result)
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
success = true log('ntqq api call failed', result)
reject(`ntqq api call failed, ${result.errMsg}`) reject(`ntqq api call failed, ${result.errMsg}`)
} }
} }
} }
setTimeout(() => { setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName)
if (!success) { if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs) log(`ntqq api timeout ${channel}, ${eventName}, ${params.methodName}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`) reject(`ntqq api timeout ${channel}, ${eventName}, ${params.methodName}, ${apiArgs}`)
} }
}, _timeout) }, timeout)
ipcMain.emit( ipcMain.emit(
channel, channel,
@@ -197,30 +177,4 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
apiArgs, apiArgs,
) )
}) })
} }
export interface GeneralCallResult {
result: number // 0: success
errMsg: string
}
export class NTQQApi {
static async call(className: NTQQApiClass, cmdName: string, args: any[]) {
return await callNTQQApi<GeneralCallResult>({
className,
methodName: cmdName,
args: [...args],
})
}
static async fetchUnitedCommendConfig() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
args: [
{
groups: ['100243'],
},
],
})
}
}

View File

@@ -1,3 +1,4 @@
export * from './common'
export * from './NodeIKernelBuddyService' export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService' export * from './NodeIKernelProfileService'
export * from './NodeIKernelGroupService' export * from './NodeIKernelGroupService'

View File

@@ -45,7 +45,7 @@ export enum GroupMemberRole {
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle: string memberSpecialTitle?: string
avatarPath: string avatarPath: string
cardName: string cardName: string
cardType: number cardType: number
@@ -60,4 +60,7 @@ export interface GroupMember {
isRobot: boolean isRobot: boolean
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean
joinTime: string
lastSpeakTime: string
} }

View File

@@ -78,9 +78,12 @@ export interface Friend extends User {
export interface CategoryFriend { export interface CategoryFriend {
categoryId: number categoryId: number
categorySortId: number
categroyName: string categroyName: string
categroyMbCount: number categroyMbCount: number
buddyList: User[] onlineCount: number
buddyList: User[] // V1
buddyUids: string[]
} }
export interface CoreInfo { export interface CoreInfo {

View File

@@ -10,7 +10,7 @@ export class OB11Response {
data: data, data: data,
message: message, message: message,
wording: message, wording: message,
echo: null, echo: undefined,
} }
} }

View File

@@ -23,66 +23,12 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44 // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const { enableLocalFile2Url } = getConfigUtil().getConfig() const { enableLocalFile2Url } = getConfigUtil().getConfig()
let UuidData: {
high: string
low: string
} | undefined
try {
UuidData = UUIDConverter.decode(payload.file)
if (UuidData) {
const peerUin = UuidData.high
const msgId = UuidData.low
const isGroup: boolean = !!(await NTQQGroupApi.getGroups(false)).find(e => e.groupCode == peerUin)
let peer: Peer | undefined
//识别Peer
if (isGroup) {
peer = { chatType: ChatType.group, peerUid: peerUin }
}
const PeerUid = await NTQQUserApi.getUidByUinV2(peerUin)
if (PeerUid) {
const isBuddy = await NTQQFriendApi.isBuddy(PeerUid)
if (isBuddy) {
peer = { chatType: ChatType.friend, peerUid: PeerUid }
} else {
peer = { chatType: ChatType.temp, peerUid: PeerUid }
}
}
if (!peer) {
throw new Error('chattype not support')
}
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId])
if (msgList.msgList.length === 0) {
throw new Error('msg not found')
}
const msg = msgList.msgList[0]
const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT)
if (!findEle) {
throw new Error('element not found')
}
const downloadPath = await NTQQFileApi.downloadMedia(msgId, msg.chatType, msg.peerUid, findEle.elementId, '', '')
const fileSize = findEle?.videoElement?.fileSize || findEle?.fileElement?.fileSize || findEle?.pttElement?.fileSize || '0'
const fileName = findEle?.videoElement?.fileName || findEle?.fileElement?.fileName || findEle?.pttElement?.fileName || ''
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: fileSize,
file_name: fileName,
}
if (enableLocalFile2Url && downloadPath) {
try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) {
throw new Error('文件下载失败. ' + e)
}
}
//不手动删除?文件持久化了
return res
}
} catch {
let fileCache = await MessageUnique.getFileCacheById(String(payload.file))
if (!fileCache?.length) {
fileCache = await MessageUnique.getFileCacheByName(String(payload.file))
} }
const fileCache = await MessageUnique.getFileCache(String(payload.file))
if (fileCache?.length) { if (fileCache?.length) {
const downloadPath = await NTQQFileApi.downloadMedia( const downloadPath = await NTQQFileApi.downloadMedia(
fileCache[0].msgId, fileCache[0].msgId,
@@ -117,7 +63,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
} else if (fileCache[0].elementType === ElementType.VIDEO) { } else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId) res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
} }
if (enableLocalFile2Url && downloadPath && res.file === res.url) { if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {
try { try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64') res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,17 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload {
group_id: string | number
file_id: string
busid?: 102
}
export class GoCQHTTPDelGroupFile extends BaseAction<Payload, void> {
actionName = ActionName.GoCQHTTP_DelGroupFile
async _handle(payload: Payload) {
await NTQQGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id])
}
}

View File

@@ -25,15 +25,16 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
const count = payload.count || 20 const count = payload.count || 20
const isReverseOrder = payload.reverseOrder || true const isReverseOrder = payload.reverseOrder || true
const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() } const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[] let msgList: RawMessage[] | undefined
// 包含 message_seq 0 // 包含 message_seq 0
if (!payload.message_seq) { if (!payload.message_seq) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count)).msgList msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count))?.msgList
} else { } else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId
if (!startMsgId) throw `消息${payload.message_seq}不存在` if (!startMsgId) throw `消息${payload.message_seq}不存在`
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList
} }
if (!msgList?.length) throw '未找到消息'
if (isReverseOrder) msgList.reverse() if (isReverseOrder) msgList.reverse()
await Promise.all( await Promise.all(
msgList.map(async msg => { msgList.map(async msg => {

View File

@@ -1,6 +1,5 @@
import fs from 'node:fs' import fs from 'node:fs'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getGroup } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types' import { ChatType, SendFileElement } from '@/ntqqapi/types'
@@ -22,10 +21,6 @@ export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id?.toString()!)
if (!group) {
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file let file = payload.file
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
file = `file://${file}` file = `file://${file}`
@@ -34,8 +29,11 @@ export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
if (!downloadResult.success) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id) const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id)
await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true) await sendMsg({
chatType: ChatType.group,
peerUid: payload.group_id?.toString()!,
}, [sendFileEle], [], true)
return null return null
} }
} }

View File

@@ -1,18 +1,18 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { getGroup } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface PayloadType { interface Payload {
group_id: number group_id: number | string
} }
class GetGroupInfo extends BaseAction<PayloadType, OB11Group> { class GetGroupInfo extends BaseAction<Payload, OB11Group> {
actionName = ActionName.GetGroupInfo actionName = ActionName.GetGroupInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()) const group = (await NTQQGroupApi.getGroups()).find(e => e.groupCode == payload.group_id.toString())
if (group) { if (group) {
return OB11Constructor.group(group) return OB11Constructor.group(group)
} else { } else {

View File

@@ -1,10 +1,8 @@
import { OB11Group } from '../../types' import { OB11Group } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { groups } from '../../../common/data'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api' import { NTQQGroupApi } from '../../../ntqqapi/api'
import { log } from '../../../common/utils'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -14,14 +12,8 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (groups.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { const groupList = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload?.no_cache === 'true')
try { return OB11Constructor.groups(groupList)
const groups = await NTQQGroupApi.getGroups(true)
log('强制刷新群列表, 数量:', groups.length)
return OB11Constructor.groups(groups)
} catch (e) {}
}
return OB11Constructor.groups(groups)
} }
} }

View File

@@ -1,30 +1,44 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { getGroupMember } from '../../../common/data' import { getGroupMember, getSelfUid } from '@/common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils/log' import { isNull } from '@/common/utils/helper'
import { isNull } from '../../../common/utils/helper'
export interface PayloadType { interface Payload {
group_id: number group_id: number | string
user_id: number user_id: number | string
} }
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> { class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()) const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) { if (member) {
if (isNull(member.sex)) { if (isNull(member.sex)) {
log('获取群成员详细信息') //log('获取群成员详细信息')
let info = await NTQQUserApi.getUserDetailInfo(member.uid, true) const info = await NTQQUserApi.getUserDetailInfo(member.uid, true)
log('群成员详细信息结果', info) //log('群成员详细信息结果', info)
Object.assign(member, info) Object.assign(member, info)
} }
return OB11Constructor.groupMember(payload.group_id.toString(), member) const ret = OB11Constructor.groupMember(payload.group_id.toString(), member)
const self = await getGroupMember(payload.group_id.toString(), getSelfUid())
if (self?.role === 3 || self?.role === 4) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString())
const target = webGroupMembers.find(e => e?.uin && e.uin === ret.user_id)
if (target) {
ret.join_time = target.join_time
ret.last_sent_time = target.last_speak_time
ret.qage = target.qage
ret.level = target.lv.level.toString()
}
}
const date = Math.round(Date.now() / 1000)
ret.last_sent_time ||= Number(member.lastSpeakTime || date)
ret.join_time ||= Number(member.joinTime || date)
return ret
} else { } else {
throw `群成员${payload.user_id}不存在` throw `群成员${payload.user_id}不存在`
} }

View File

@@ -1,31 +1,58 @@
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { getGroup } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi, WebApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils' import { getSelfUid } from '@/common/data'
export interface PayloadType { interface Payload {
group_id: number group_id: number | string
no_cache: boolean | string no_cache: boolean | string
} }
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> { class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList actionName = ActionName.GetGroupMemberList
protected async _handle(payload: PayloadType) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()) const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
if (group) { const groupMembersArr = Array.from(groupMembers.values())
if (!group.members?.length || payload.no_cache === true || payload.no_cache === 'true') {
const members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) let _groupMembers = groupMembersArr.map(item => {
group.members = Array.from(members.values()) return OB11Constructor.groupMember(payload.group_id.toString(), item)
log('强制刷新群成员列表, 数量: ', group.members.length) })
}
return OB11Constructor.groupMembers(group) const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
} else { const date = Math.round(Date.now() / 1000)
throw `${payload.group_id}不存在`
for (let i = 0, len = _groupMembers.length; i < len; i++) {
// 保证基础数据有这个 同时避免群管插件过于依赖这个杀了
_groupMembers[i].join_time = date
_groupMembers[i].last_sent_time = date
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i])
} }
const selfRole = groupMembers.get(getSelfUid())?.role
const isPrivilege = selfRole === 3 || selfRole === 4
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString())
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) {
continue
}
const MemberData = MemberMap.get(webGroupMembers[i]?.uin)
if (MemberData) {
MemberData.join_time = webGroupMembers[i]?.join_time
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time
MemberData.qage = webGroupMembers[i]?.qage
MemberData.level = webGroupMembers[i]?.lv.level.toString()
MemberMap.set(webGroupMembers[i]?.uin, MemberData)
}
}
}
_groupMembers = Array.from(MemberMap.values())
return _groupMembers
} }
} }

View File

@@ -1,6 +1,6 @@
import GetMsg from './msg/GetMsg' import GetMsg from './msg/GetMsg'
import GetLoginInfo from './system/GetLoginInfo' import GetLoginInfo from './system/GetLoginInfo'
import { GetFriendList, GetFriendWithCategory} from './user/GetFriendList' import { GetFriendList, GetFriendWithCategory } from './user/GetFriendList'
import GetGroupList from './group/GetGroupList' import GetGroupList from './group/GetGroupList'
import GetGroupInfo from './group/GetGroupInfo' import GetGroupInfo from './group/GetGroupInfo'
import GetGroupMemberList from './group/GetGroupMemberList' import GetGroupMemberList from './group/GetGroupMemberList'
@@ -53,6 +53,7 @@ import { GoCQHTTHandleQuickOperation } from './go-cqhttp/QuickOperation'
import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg' import GoCQHTTPSetEssenceMsg from './go-cqhttp/SetEssenceMsg'
import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg' import GoCQHTTPDelEssenceMsg from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent' import GetEvent from './llonebot/GetEvent'
import { GoCQHTTPDelGroupFile } from './go-cqhttp/DelGroupFile'
export const actionHandlers = [ export const actionHandlers = [
@@ -113,7 +114,8 @@ export const actionHandlers = [
new GoCQHTTGetForwardMsgAction(), new GoCQHTTGetForwardMsgAction(),
new GoCQHTTHandleQuickOperation(), new GoCQHTTHandleQuickOperation(),
new GoCQHTTPSetEssenceMsg(), new GoCQHTTPSetEssenceMsg(),
new GoCQHTTPDelEssenceMsg() new GoCQHTTPDelEssenceMsg(),
new GoCQHTTPDelGroupFile()
] ]
function initActionMap() { function initActionMap() {

View File

@@ -6,7 +6,7 @@ import {
RawMessage, RawMessage,
SendMessageElement, SendMessageElement,
} from '@/ntqqapi/types' } from '@/ntqqapi/types'
import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '@/common/data' import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data'
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
@@ -289,12 +289,12 @@ export async function sendMsg(
log('文件大小计算失败', e, fileElement) log('文件大小计算失败', e, fileElement)
} }
} }
log('发送消息总大小', totalSize, 'bytes') //log('发送消息总大小', totalSize, 'bytes')
let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/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)
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
log('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path)) deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg return returnMsg
} }
@@ -305,10 +305,9 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
// This redundant design of Ob11 here should be blamed. // This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) { if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
const group = (await getGroup(payload.group_id))! // checked before
return { return {
chatType: ChatType.group, chatType: ChatType.group,
peerUid: group.groupCode peerUid: payload.group_id.toString(),
} }
} }
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) { if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
@@ -318,7 +317,7 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
return { return {
chatType: isBuddy ? ChatType.friend : ChatType.temp, chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!, peerUid: Uid!,
guildId: payload.group_id || ''//临时主动发起时需要传入群号 guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
} }
} }
throw '请指定 group_id 或 user_id' throw '请指定 group_id 或 user_id'
@@ -343,12 +342,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
message: '音乐消息不可以和其他消息混在一起发送', message: '音乐消息不可以和其他消息混在一起发送',
} }
} }
if (payload.message_type !== 'private' && payload.group_id && !(await getGroup(payload.group_id))) {
return {
valid: false,
message: `${payload.group_id}不存在`,
}
}
if (payload.user_id && payload.message_type !== 'group') { if (payload.user_id && payload.message_type !== 'group') {
const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString()) const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
const isBuddy = await NTQQFriendApi.isBuddy(uid!) const isBuddy = await NTQQFriendApi.isBuddy(uid!)
@@ -363,7 +356,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
protected async _handle(payload: OB11PostSendMsg) { protected async _handle(payload: OB11PostSendMsg) {
const peer = await createContext(payload, ContextMode.Normal) let contextMode = ContextMode.Normal
if (payload.message_type === 'group') {
contextMode = ContextMode.Group
} else if (payload.message_type === 'private') {
contextMode = ContextMode.Private
}
const peer = await createContext(payload, contextMode)
const messages = convertMessage2List( const messages = convertMessage2List(
payload.message, payload.message,
payload.auto_escape === true || payload.auto_escape === 'true', payload.auto_escape === true || payload.auto_escape === 'true',

View File

@@ -73,4 +73,5 @@ export enum ActionName {
GetGroupHonorInfo = "get_group_honor_info", GetGroupHonorInfo = "get_group_honor_info",
GoCQHTTP_SetEssenceMsg = 'set_essence_msg', GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
GoCQHTTP_DelEssenceMsg = 'delete_essence_msg', GoCQHTTP_DelEssenceMsg = 'delete_essence_msg',
GoCQHTTP_DelGroupFile = 'delete_group_file',
} }

View File

@@ -15,10 +15,13 @@ export class GetCookies extends BaseAction<Payload, Response> {
actionName = ActionName.GetCookies actionName = ActionName.GetCookies
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (!payload.domain) {
throw '缺少参数 domain'
}
const cookiesObject = await NTQQUserApi.getCookies(payload.domain) const cookiesObject = await NTQQUserApi.getCookies(payload.domain)
//把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起 //把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const bkn = WebApi.genBkn(cookiesObject.p_skey) const bkn = cookiesObject.skey ? WebApi.genBkn(cookiesObject.skey) : ''
return { cookies, bkn } return { cookies, bkn }
} }
} }

View File

@@ -3,8 +3,8 @@ import { ActionName } from '../types'
import { NTQQUserApi } from '@/ntqqapi/api' import { NTQQUserApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
user_id: number user_id: number | string
times: number times: number | string
} }
export default class SendLike extends BaseAction<Payload, null> { export default class SendLike extends BaseAction<Payload, null> {
@@ -14,9 +14,9 @@ export default class SendLike extends BaseAction<Payload, null> {
try { try {
const qq = payload.user_id.toString() const qq = payload.user_id.toString()
const uid: string = await NTQQUserApi.getUidByUin(qq) || '' const uid: string = await NTQQUserApi.getUidByUin(qq) || ''
const result = await NTQQUserApi.like(uid, parseInt(payload.times?.toString()) || 1) const result = await NTQQUserApi.like(uid, +payload.times || 1)
if (result.result !== 0) { if (result?.result !== 0) {
throw Error(result.errMsg) throw Error(result?.errMsg)
} }
} catch (e) { } catch (e) {
throw `点赞失败 ${e}` throw `点赞失败 ${e}`

View File

@@ -17,7 +17,6 @@ import {
Group, Group,
Peer, Peer,
GroupMember, GroupMember,
PicType,
RawMessage, RawMessage,
SelfInfo, SelfInfo,
Sex, Sex,
@@ -26,11 +25,10 @@ import {
FriendV2, FriendV2,
ChatType2 ChatType2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getGroupMember, getSelfUin } from '../common/data' import { getGroupMember, getSelfUin } from '../common/data'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { MessageUnique } from '../common/utils/MessageUnique' import { MessageUnique } from '../common/utils/MessageUnique'
import { UUIDConverter } from '../common/utils/helper'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent' import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent' import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent' import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
@@ -51,6 +49,7 @@ import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNotice
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent' import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent'
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
import { omit } from 'cosmokit'
export class OB11Constructor { export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { static async message(msg: RawMessage): Promise<OB11Message> {
@@ -100,7 +99,7 @@ export class OB11Constructor {
else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) { else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group' resMsg.sub_type = 'group'
const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid) const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid)
if (ret.result === 0) { if (ret?.result === 0) {
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode) resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
} else { } else {
@@ -159,20 +158,20 @@ export class OB11Constructor {
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
chatType: msg.chatType, chatType: msg.chatType,
}, element.replyElement.replayMsgSeq, 1, true, true)).msgList[0] }, element.replyElement.replayMsgSeq, 1, true, true))?.msgList[0]
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
const peer = { const peer = {
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
} }
replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq)).msgList[0] replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq))?.msgList[0]
} }
// 284840486: 合并消息内侧 消息具体定位不到 // 284840486: 合并消息内侧 消息具体定位不到
if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') {
throw new Error('回复消息消息验证失败') throw new Error('回复消息消息验证失败')
} }
message_data['data']['id'] = MessageUnique.createMsg({ message_data['data']['id'] = replyMsg && MessageUnique.createMsg({
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
chatType: msg.chatType, chatType: msg.chatType,
@@ -192,41 +191,61 @@ export class OB11Constructor {
}*/ }*/
message_data['data']['file'] = picElement.fileName message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = picElement.picSubType message_data['data']['subType'] = picElement.picSubType
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) //message_data['data']['file_id'] = picElement.fileUuid
message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement) message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement)
message_data['data']['file_size'] = picElement.fileSize message_data['data']['file_size'] = picElement.fileSize
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType, chatType: msg.chatType,
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: picElement.fileName, fileName: picElement.fileName,
fileSize: String(picElement.fileSize || '0'), fileSize: String(picElement.fileSize || '0'),
fileUuid: picElement.fileUuid
}) })
} }
else if (element.videoElement || element.fileElement) { else if (element.videoElement) {
const videoOrFileElement = element.videoElement || element.fileElement message_data['type'] = OB11MessageDataType.video
message_data['type'] = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file const { videoElement } = element
message_data['data']['file'] = videoOrFileElement.fileName message_data['data']['file'] = videoElement.fileName
message_data['data']['path'] = videoOrFileElement.filePath message_data['data']['path'] = videoElement.filePath
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) //message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize message_data['data']['file_size'] = videoElement.fileSize
if (element.videoElement) { message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ chatType: msg.chatType,
chatType: msg.chatType, peerUid: msg.peerUid,
peerUid: msg.peerUid, }, msg.msgId, element.elementId)
}, msg.msgId, element.elementId,
)
}
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType, chatType: msg.chatType,
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: videoOrFileElement.fileName, fileName: videoElement.fileName,
fileSize: String(videoOrFileElement.fileSize || '0') fileSize: String(videoElement.fileSize || '0'),
fileUuid: videoElement.fileUuid!
})
}
else if (element.fileElement) {
message_data['type'] = OB11MessageDataType.file
const { fileElement } = element
message_data['data']['file'] = fileElement.fileName
message_data['data']['path'] = fileElement.filePath
message_data['data']['file_id'] = fileElement.fileUuid
message_data['data']['file_size'] = fileElement.fileSize
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: fileElement.fileName,
fileSize: String(fileElement.fileSize || '0'),
fileUuid: fileElement.fileUuid!
}) })
} }
else if (element.pttElement) { else if (element.pttElement) {
@@ -234,16 +253,18 @@ export class OB11Constructor {
const { pttElement } = element const { pttElement } = element
message_data['data']['file'] = pttElement.fileName message_data['data']['file'] = pttElement.fileName
message_data['data']['path'] = pttElement.filePath message_data['data']['path'] = pttElement.filePath
message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId) //message_data['data']['file_id'] = pttElement.fileUuid
message_data['data']['file_size'] = pttElement.fileSize message_data['data']['file_size'] = pttElement.fileSize
MessageUnique.addFileCache({ MessageUnique.addFileCache({
peerUid: msg.peerUid, peerUid: msg.peerUid,
msgId: msg.msgId, msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType, chatType: msg.chatType,
elementId: element.elementId, elementId: element.elementId,
elementType: element.elementType, elementType: element.elementType,
fileName: pttElement.fileName, fileName: pttElement.fileName,
fileSize: String(pttElement.fileSize || '0') fileSize: String(pttElement.fileSize || '0'),
fileUuid: pttElement.fileUuid
}) })
} }
else if (element.arkElement) { else if (element.arkElement) {
@@ -358,7 +379,7 @@ export class OB11Constructor {
const groupElement = grayTipElement?.groupElement const groupElement = grayTipElement?.groupElement
if (groupElement) { if (groupElement) {
// log("收到群提示消息", groupElement) // log("收到群提示消息", groupElement)
if (groupElement.type == TipGroupElementType.memberIncrease) { if (groupElement.type === TipGroupElementType.memberIncrease) {
log('收到群成员增加消息', groupElement) log('收到群成员增加消息', groupElement)
await sleep(1000) await sleep(1000)
const member = await getGroupMember(msg.peerUid, groupElement.memberUid) const member = await getGroupMember(msg.peerUid, groupElement.memberUid)
@@ -406,25 +427,26 @@ export class OB11Constructor {
) )
} }
} }
else if (groupElement.type == TipGroupElementType.kicked) { else if (groupElement.type === TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
deleteGroup(msg.peerUid)
NTQQGroupApi.quitGroup(msg.peerUid).then() NTQQGroupApi.quitGroup(msg.peerUid).then()
const selfUin = getSelfUin()
try { try {
const adminUin = const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await NTQQUserApi.getUidByUin(groupElement.adminUid))
(await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin
if (adminUin) { if (adminUin) {
return new OB11GroupDecreaseEvent( return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(selfUin), parseInt(getSelfUin()),
parseInt(adminUin), parseInt(adminUin),
'kick_me', 'kick_me'
) )
} }
} catch (e) { } catch (e) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfUin), 0, 'leave') return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(getSelfUin()),
0,
'leave'
)
} }
} }
} }
@@ -462,8 +484,8 @@ export class OB11Constructor {
chatType: ChatType.group, chatType: ChatType.group,
guildId: '', guildId: '',
peerUid: msg.peerUid, peerUid: msg.peerUid,
}, msgSeq, 1, true, true)).msgList }, msgSeq, 1, true, true))?.msgList
if (replyMsgList.length < 1) { if (!replyMsgList?.length) {
return return
} }
const likes = [ const likes = [
@@ -556,7 +578,10 @@ export class OB11Constructor {
chatType: ChatType.group, chatType: ChatType.group,
peerUid: Group! peerUid: Group!
} }
const { msgList } = await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true) const msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true))?.msgList
if (!msgList?.length) {
return
}
//const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId) //const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
//const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg //const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息 // 如果 senderUin 为 0可能是 历史消息 或 自身消息
@@ -637,7 +662,7 @@ export class OB11Constructor {
for (const friend of friends) { for (const friend of friends) {
const sexValue = this.sex(friend.baseInfo.sex!) const sexValue = this.sex(friend.baseInfo.sex!)
data.push({ data.push({
...friend.baseInfo, ...omit(friend.baseInfo, ['richBuffer']),
...friend.coreInfo, ...friend.coreInfo,
user_id: parseInt(friend.coreInfo.uin), user_id: parseInt(friend.coreInfo.uin),
nickname: friend.coreInfo.nick, nickname: friend.coreInfo.nick,
@@ -677,7 +702,7 @@ export class OB11Constructor {
sex: OB11Constructor.sex(member.sex!), sex: OB11Constructor.sex(member.sex!),
age: 0, age: 0,
area: '', area: '',
level: 0, level: '0',
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0, qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
join_time: 0, // 暂时没法获取 join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取 last_sent_time: 0, // 暂时没法获取

View File

@@ -10,23 +10,27 @@ abstract class OB11PokeEvent extends OB11BaseNoticeEvent {
export class OB11FriendPokeEvent extends OB11PokeEvent { export class OB11FriendPokeEvent extends OB11PokeEvent {
user_id: number user_id: number
raw_info: any
constructor(user_id: number, target_id: number, raw_message: any) { constructor(user_id: number, target_id: number, raw_message: any) {
super(); super()
this.target_id = target_id; this.target_id = target_id
this.user_id = user_id; this.user_id = user_id
this.raw_message = raw_message; // raw_message nb等框架标准为string
this.raw_info = raw_message
} }
} }
export class OB11GroupPokeEvent extends OB11PokeEvent { export class OB11GroupPokeEvent extends OB11PokeEvent {
user_id: number user_id: number
group_id: number group_id: number
raw_info: any
constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) { constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) {
super() super()
this.group_id = group_id this.group_id = group_id
this.target_id = target_id this.target_id = target_id
this.user_id = user_id this.user_id = user_id
this.raw_message = raw_message this.raw_info = raw_message
} }
} }

View File

@@ -27,9 +27,10 @@ export function unregisterWsEventSender(ws: WebSocketClass) {
export function postWsEvent(event: PostEventType) { export function postWsEvent(event: PostEventType) {
for (const ws of eventWSList) { for (const ws of eventWSList) {
new Promise(() => { new Promise((resolve) => {
wsReply(ws, event) wsReply(ws, event)
}).then().catch(log) resolve(undefined)
}).then()
} }
} }
@@ -61,13 +62,15 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
body: msgStr, body: msgStr,
}).then( }).then(
async (res) => { async (res) => {
log(`新消息事件HTTP上报成功: ${host} `, msgStr) if (msg.post_type) {
log(`HTTP 事件上报: ${host} `, msg.post_type)
}
try { try {
const resJson = await res.json() const resJson = await res.json()
log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson)) log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson))
handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log); handleQuickOperation(msg as QuickOperationEvent, resJson).then().catch(log);
} catch (e) { } catch (e) {
log(`新消息事件HTTP上报没有返回快速操作不需要处理`) //log(`新消息事件HTTP上报没有返回快速操作不需要处理`)
return return
} }
}, },

View File

@@ -104,6 +104,10 @@ export class ReverseWebsocket {
this.websocket.on('error', log) this.websocket.on('error', log)
this.websocket.on('ping',()=>{
this.websocket?.pong()
})
const wsClientInterval = setInterval(() => { const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!)) postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!))
}, heartInterval) // 心跳包 }, heartInterval) // 心跳包

View File

@@ -1,70 +1,131 @@
import { WebSocket } from 'ws' import BaseAction from '../../action/BaseAction'
import { WebSocket, WebSocketServer } from 'ws'
import { actionMap } from '../../action' import { actionMap } from '../../action'
import { OB11Response } from '../../action/OB11Response' import { OB11Response } from '../../action/OB11Response'
import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../post-ob11-event' import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../post-ob11-event'
import { ActionName } from '../../action/types' import { ActionName } from '../../action/types'
import BaseAction from '../../action/BaseAction'
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent'
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent'
import { WebsocketServerBase } from '../../../common/server/websocket'
import { IncomingMessage } from 'node:http' import { IncomingMessage } from 'node:http'
import { wsReply } from './reply' import { wsReply } from './reply'
import { getSelfInfo } from '../../../common/data' import { getSelfInfo } from '@/common/data'
import { log } from '../../../common/utils/log' import { log } from '@/common/utils/log'
import { getConfigUtil } from '../../../common/config' import { getConfigUtil } from '@/common/config'
import { llonebotError } from '@/common/data'
class OB11WebsocketServer extends WebsocketServerBase { export class OB11WebsocketServer {
authorizeFailed(wsClient: WebSocket) { private ws?: WebSocketServer
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')))
constructor() {
log(`llonebot websocket service started`)
} }
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) { start(port: number) {
try {
this.ws = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 1024 })
llonebotError.wsServerError = ''
} catch (e: any) {
llonebotError.wsServerError = '正向 WebSocket 服务启动失败, ' + e.toString()
return
}
this.ws?.on('connection', (socket, req) => {
const url = req.url?.split('?').shift()
this.authorize(socket, req)
this.onConnect(socket, url!)
})
}
stop() {
llonebotError.wsServerError = ''
this.ws?.close(err => {
log('ws server close failed!', err)
})
this.ws = undefined
}
restart(port: number) {
this.stop()
this.start(port)
}
private authorize(socket: WebSocket, req: IncomingMessage) {
const { token } = getConfigUtil().getConfig()
const url = req.url?.split('?').shift()
log('ws connect', url)
let clientToken = ''
const authHeader = req.headers['authorization']
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()!
log('receive ws header token', clientToken)
} else {
const { searchParams } = new URL(`http://localhost${req.url}`)
const urlToken = searchParams.get('access_token')
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log('receive ws url token', clientToken)
}
}
if (token && clientToken !== token) {
this.authorizeFailed(socket)
return socket.close()
}
}
private authorizeFailed(socket: WebSocket) {
socket.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')))
}
private async handleAction(socket: WebSocket, actionName: string, params: any, echo?: any) {
const action: BaseAction<any, any> = actionMap.get(actionName)! const action: BaseAction<any, any> = actionMap.get(actionName)!
if (!action) { if (!action) {
return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo)) return wsReply(socket, OB11Response.error('不支持的api ' + actionName, 1404, echo))
} }
try { try {
let handleResult = await action.websocketHandle(params, echo) const handleResult = await action.websocketHandle(params, echo)
handleResult.echo = echo handleResult.echo = echo
wsReply(wsClient, handleResult) wsReply(socket, handleResult)
} catch (e: any) { } catch (e: any) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo)) wsReply(socket, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))
} }
} }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { private onConnect(socket: WebSocket, url: string) {
if (url == '/api' || url == '/api/' || url == '/') { if (['/api', '/api/', '/'].includes(url)) {
wsClient.on('message', async (msg) => { socket.on('message', async (msg) => {
let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} } let receiveData: { action: ActionName | null; params: any; echo?: any } = { action: null, params: {} }
let echo = null let echo: any
try { try {
receiveData = JSON.parse(msg.toString()) receiveData = JSON.parse(msg.toString())
echo = receiveData.echo echo = receiveData.echo
log('收到正向Websocket消息', receiveData) log('收到正向Websocket消息', receiveData)
} catch (e) { } catch (e) {
return wsReply(wsClient, OB11Response.error('json解析失败请检查数据格式', 1400, echo)) return wsReply(socket, OB11Response.error('json解析失败请检查数据格式', 1400, echo))
} }
this.handleAction(wsClient, receiveData.action!, receiveData.params, receiveData.echo).then() this.handleAction(socket, receiveData.action!, receiveData.params, receiveData.echo)
}) })
} }
if (url == '/event' || url == '/event/' || url == '/') { if (['/event', '/event/', '/'].includes(url)) {
registerWsEventSender(wsClient) registerWsEventSender(socket)
log('event上报ws客户端已连接') log('event上报ws客户端已连接')
try { try {
wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) wsReply(socket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} catch (e) { } catch (e) {
log('发送生命周期失败', e) log('发送生命周期失败', e)
} }
const { heartInterval } = getConfigUtil().getConfig() const { heartInterval } = getConfigUtil().getConfig()
const wsClientInterval = setInterval(() => { const intervalId = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!)) postWsEvent(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!))
}, heartInterval) // 心跳包 }, heartInterval) // 心跳包
wsClient.on('close', () => { socket.on('close', () => {
log('event上报ws客户端已断开') log('event上报ws客户端已断开')
clearInterval(wsClientInterval) clearInterval(intervalId)
unregisterWsEventSender(wsClient) unregisterWsEventSender(socket)
}) })
} }
} }

View File

@@ -1,18 +1,15 @@
import { WebSocket as WebSocketClass } from 'ws' import { WebSocket as WebSocketClass } from 'ws'
import { OB11Response } from '../../action/OB11Response'
import { PostEventType } from '../post-ob11-event' import { PostEventType } from '../post-ob11-event'
import { log } from '../../../common/utils/log' import { log } from '@/common/utils/log'
import { isNull } from '../../../common/utils/helper' import { OB11Return } from '../../types'
export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) { export function wsReply(wsClient: WebSocketClass, data: OB11Return<any> | PostEventType) {
try { try {
const packet = Object.assign({}, data) wsClient.send(JSON.stringify(data))
if (isNull(packet['echo'])) { if (data['post_type']) {
delete packet['echo'] log('WebSocket 事件上报', wsClient.url ?? '', data['post_type'])
} }
wsClient.send(JSON.stringify(packet))
//log('ws 消息上报', wsClient.url || '', data)
} catch (e: any) { } catch (e: any) {
log('websocket 回复失败', e.stack, data) log('WebSocket 上报失败', e.stack, data)
} }
} }

View File

@@ -36,7 +36,7 @@ export interface OB11GroupMember {
age?: number age?: number
join_time?: number join_time?: number
last_sent_time?: number last_sent_time?: number
level?: number level?: string
qq_level?: number qq_level?: number
role?: OB11GroupMemberRole role?: OB11GroupMemberRole
title?: string title?: string
@@ -48,6 +48,7 @@ export interface OB11GroupMember {
shut_up_timestamp?: number shut_up_timestamp?: number
// 以下为扩展字段 // 以下为扩展字段
is_robot?: boolean is_robot?: boolean
qage?: number
} }
export interface OB11Group { export interface OB11Group {

View File

@@ -1 +1 @@
export const version = '3.29.2' export const version = '3.30.4'