Compare commits

..

65 Commits

Author SHA1 Message Date
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
idranme
0182803ae1 Merge pull request #339 from LLOneBot/dev
3.29.2
2024-08-15 11:14:35 +08:00
idranme
94c1aea6df chore: v3.29.2 2024-08-15 10:57:15 +08:00
idranme
d143dc043c fix 2024-08-15 10:31:51 +08:00
idranme
3f4b0b44cf feat: cache recalled message content 2024-08-14 23:04:15 +08:00
idranme
26fc0c68b2 Merge pull request #337 from LLOneBot/dev
3.29.1
2024-08-14 19:00:42 +08:00
idranme
c1d7aa7aed chore: v3.29.1 2024-08-14 18:59:27 +08:00
idranme
6aa44bdd79 fix: /get_image 2024-08-14 18:20:39 +08:00
idranme
77f3bfc5c5 Merge pull request #335 from LLOneBot/dev
3.29.0
2024-08-13 22:11:17 +08:00
idranme
2715552814 chore: v3.29.0 2024-08-13 22:08:36 +08:00
idranme
8ed0e6c1be fix 2024-08-13 21:59:13 +08:00
idranme
260a0be184 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-08-13 19:31:10 +08:00
idranme
6582ffe964 fix: msg 2024-08-13 19:29:22 +08:00
linyuchen
f8e231b8b8 chore: v3.28.7
fix: CPU占用过高
fix: 好友列表变动hook失败
2024-08-13 19:09:13 +08:00
idranme
a0f5cc0e36 Merge pull request #333 from LLOneBot/dev
Update README.md
2024-08-12 15:01:26 +08:00
idranme
277c2a9b67 Update README.md 2024-08-12 15:00:41 +08:00
idranme
874acdd7fe Merge pull request #331 from LLOneBot/dev
3.28.6
2024-08-12 00:03:25 +08:00
idranme
b2b996df9c chore: v3.28.6 2024-08-12 00:01:39 +08:00
idranme
4427774c2d fix: multiForwardMsg 2024-08-12 00:01:06 +08:00
idranme
41c04faa05 Merge pull request #330 from LLOneBot/dev
3.28.5
2024-08-11 20:01:29 +08:00
idranme
6ad4492f01 chore: v3.28.5 2024-08-11 20:00:47 +08:00
idranme
d52f16bc88 opt 2024-08-11 19:42:44 +08:00
idranme
2b0179acd1 opt 2024-08-11 18:10:27 +08:00
idranme
f540f324a1 Merge pull request #329 from LLOneBot/dev
3.28.4
2024-08-11 12:21:37 +08:00
idranme
128f40a51d chore: v3.28.4 2024-08-11 12:17:47 +08:00
idranme
c815e0ca6b sync 2024-08-11 12:16:53 +08:00
idranme
1da720e0a7 sync 2024-08-11 02:43:14 +08:00
idranme
1472c9c949 opt 2024-08-11 00:23:17 +08:00
idranme
4678253815 sync 2024-08-11 00:18:54 +08:00
idranme
e1176e18cd Merge pull request #328 from LLOneBot/dev
3.28.3
2024-08-10 23:19:09 +08:00
idranme
107f02f21f chore: 3.28.3 2024-08-10 23:17:38 +08:00
idranme
51f8db3a83 opt 2024-08-10 22:31:14 +08:00
idranme
25691a4124 sync 2024-08-10 22:09:35 +08:00
idranme
40f03e6401 sync 2024-08-10 21:34:28 +08:00
idranme
9f89094978 sync 2024-08-10 20:36:15 +08:00
idranme
04f837145c sync 2024-08-10 18:14:33 +08:00
idranme
6126920830 sync 2024-08-10 17:17:19 +08:00
idranme
5c219aa003 opt 2024-08-09 22:32:54 +08:00
idranme
ce5cf82339 Merge pull request #325 from LLOneBot/dev
3.28.2
2024-08-09 18:10:50 +08:00
idranme
6931277e33 chore: v3.28.2 2024-08-09 18:07:50 +08:00
idranme
be1b9c21c1 feat: support for at message segment specifying name 2024-08-09 18:02:52 +08:00
idranme
b02cd3af00 Create .editorconfig 2024-08-09 16:46:08 +08:00
idranme
22dcbac16f Merge pull request #324 from LLOneBot/dev
fix ci
2024-08-09 16:06:44 +08:00
idranme
44faedd6c0 fix ci 2024-08-09 16:05:51 +08:00
idranme
fb3b673e63 Merge pull request #323 from LLOneBot/dev
fix ci
2024-08-09 15:53:42 +08:00
idranme
4e377f86d1 fix ci 2024-08-09 15:53:04 +08:00
idranme
e8bd98020b Merge pull request #322 from LLOneBot/dev
v3.28.1
2024-08-09 15:49:29 +08:00
idranme
c520034934 chore: v3.28.1 2024-08-09 15:47:57 +08:00
idranme
5d5fd403b8 fix: filtering at segments when sending private chat messages 2024-08-09 15:44:18 +08:00
idranme
1fc02229df sync 2024-08-09 15:40:08 +08:00
idranme
6c8d3db3a4 opt 2024-08-09 14:26:30 +08:00
idranme
c5b69561af sync 2024-08-09 14:20:59 +08:00
idranme
b5bffff941 fix 2024-08-07 23:17:13 +08:00
idranme
1a2cdc8c0e opt 2024-08-07 22:08:47 +08:00
idranme
50ab62f103 opt: config 2024-08-07 21:39:26 +08:00
idranme
5005d83ce0 opt: audio encoding and decoding 2024-08-07 04:22:51 +08:00
idranme
d7e40e488c Update README.md
LLAPI 已删库
2024-08-06 22:31:39 +08:00
idranme
4958e22770 Update README.md 2024-08-06 22:28:49 +08:00
idranme
a5e3f94228 chore: deps 2024-08-06 22:26:21 +08:00
idranme
9e57b2c17e Update publish.yml 2024-08-06 14:51:17 +08:00
idranme
e1ff366e10 clean 2024-08-06 02:32:28 +08:00
94 changed files with 4968 additions and 2556 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
insert_final_newline = true
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

View File

@@ -27,7 +27,6 @@ jobs:
- name: zip - name: zip
run: | run: |
sudo apt install zip -y sudo apt install zip -y
cp manifest.json ./dist/manifest.json
cd ./dist/ cd ./dist/
zip -r ../LLOneBot.zip ./* zip -r ../LLOneBot.zip ./*

View File

@@ -1,14 +0,0 @@
# 3.24.0
## 修复
* 修复图片rkey导致链接失效的问题
* 修复/get_image, /get_file 无法获取图片的问题
* 修复上报他人管理员被取消通知
## 新增
* 新增表情回应发送和上报
* 新增商城表情发送,和上报 url
* 新增转发单条消息接口 `forward_friend_single_msg`, `forward_group_single_msg`
* 新增新增好友事件

View File

@@ -3,7 +3,7 @@
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** > **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG群<https://t.me/+nLZEnpne-pQ1OWFl>
@@ -23,29 +23,15 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录
- [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息
- [x] 文件消息
- [x] 群禁言事件上报
- [x] 优化加群成功事件上报
- [x] 清理缓存api
- [ ] 框架对接文档
## Stargazers over time ## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot) [![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
## 鸣谢 ## 鸣谢
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) - [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) - [chronocat](https://github.com/chrononeko/chronocat)
- [chronocat](https://github.com/chrononeko/chronocat/)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) - [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm) - [silk-wasm](https://github.com/idranme/silk-wasm)

View File

@@ -5,14 +5,7 @@ import './scripts/gen-manifest'
const external = [ const external = [
'silk-wasm', 'silk-wasm',
'ws', 'ws',
'level', '@minatojs/sql.js',
'classic-level',
'abstract-level',
'level-supports',
'level-transcoder',
'module-error',
'catering',
'node-gyp-build',
] ]
function genCpModule(module: string) { function genCpModule(module: string) {

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用以 QQ 机器人开发", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发",
"version": "3.28.0", "version": "3.29.3",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
@@ -13,7 +13,7 @@
} }
], ],
"repository": { "repository": {
"repo": "linyuchen/LiteLoaderQQNT-OneBotApi", "repo": "LLOneBot/LLOneBot",
"branch": "main", "branch": "main",
"release": { "release": {
"tag": "latest", "tag": "latest",

View File

@@ -2,7 +2,7 @@
"name": "llonebot", "name": "llonebot",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"description": "NTQQLiteLoaderOneBotApi", "description": "",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"build": "electron-vite build", "build": "electron-vite build",
@@ -16,26 +16,28 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.5.0",
"compressing": "^1.10.1", "compressing": "^1.10.1",
"cordis": "^3.18.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.19.2",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^4.4.1",
"file-type": "^19.0.0", "file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.3",
"level": "^8.0.1", "minato": "^3.5.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.20", "@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.11.24", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^29.0.1", "electron": "^29.1.4",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.3.5", "vite": "^5.4.1",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.0" "packageManager": "yarn@4.4.0"

View File

@@ -16,7 +16,7 @@ const manifest = {
} }
], ],
repository: { repository: {
repo: 'linyuchen/LiteLoaderQQNT-OneBotApi', repo: 'LLOneBot/LLOneBot',
branch: 'main', branch: 'main',
release: { release: {
tag: 'latest', tag: 'latest',

View File

@@ -2,7 +2,7 @@ import fs from 'node:fs'
import { Config, OB11Config } from './types' import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper' import { mergeNewProperties } from './utils/helper'
import path from 'node:path' import path from 'node:path'
import { selfInfo } 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
@@ -52,6 +52,7 @@ export class ConfigUtil {
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
msgCacheExpire: 120
} }
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
@@ -97,6 +98,6 @@ export class ConfigUtil {
} }
export function getConfigUtil() { export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) const configFilePath = path.join(DATA_DIR, `config_${getSelfUin()}.json`)
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }

View File

@@ -1,44 +1,32 @@
import { import {
CategoryFriend,
type Friend, type Friend,
type FriendRequest,
type Group,
type GroupMember, type GroupMember,
type SelfInfo, type SelfInfo,
User,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { type FileCache, type LLOneBotError } from './types' import { type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group' import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log' import { log } from './utils/log'
import { isNumeric } from './utils/helper' import { isNumeric } from './utils/helper'
import { NTQQFriendApi } from '../ntqqapi/api' import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { WebApiGroupMember } from '@/ntqqapi/api/webapi' import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
import { getBuildVersion } from './utils/QQBasicInfo'
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export const WebGroupData = {
GroupData: new Map<string, Array<WebApiGroupMember>>(),
GroupTime: new Map<string, number>(),
}
export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
httpServerError: '', httpServerError: '',
wsServerError: '', wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误', otherError: 'LLOneBot 未能正常启动,请检查日志查看错误',
} }
// 群号 -> 群成员map(uid=>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())
@@ -52,73 +40,89 @@ 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()
let members = groupMembers.get(groupCodeStr)
if (!members) {
try { try {
const _groups = await NTQQGroupApi.getGroups(true) members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
group = _groups.find((group) => group.groupCode === qq.toString()) // 更新群成员列表
if (group) { groupMembers.set(groupCodeStr, members)
groups.push(group) }
} catch (e) {
} catch (e) { return null
} }
} }
return group const getMember = () => {
} let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUidStr)) {
export function deleteGroup(groupCode: string) { member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString()) } else {
// log(groups, groupCode, groupIndex); member = members!.get(memberUinOrUidStr)
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()
const group = await getGroup(groupQQ)
if (group) {
const filterKey = isNumeric(memberUinOrUid) ? 'uin' : 'uid'
const filterValue = memberUinOrUid
let filterFunc: (member: GroupMember) => boolean = (member) => member[filterKey] === filterValue
let member = group.members?.find(filterFunc)
if (!member) {
try {
const _members = await NTQQGroupApi.getGroupMembers(groupQQ)
if (_members.length > 0) {
group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
} }
return member return member
} }
return null let member = getMember()
} if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
export async function refreshGroupMembers(groupQQ: string) { member = getMember()
const group = groups.find((group) => group.groupCode === groupQQ)
if (group) {
group.members = await NTQQGroupApi.getGroupMembers(groupQQ)
} }
return member
} }
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号 const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export function getUidByUin(uin: string) { export async function getSelfNick(force = false): Promise<string> {
for (const uid in uidMaps) { if ((!selfInfo.nick || force) && selfInfo.uid) {
if (uidMaps[uid] === uin) { const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
return uid if (userInfo) {
selfInfo.nick = userInfo.nick
return userInfo.nick
} }
} }
return selfInfo.nick
} }
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号 export function getSelfInfo() {
return selfInfo
}
export let rawFriends: CategoryFriend[] = [] export function setSelfInfo(data: Partial<SelfInfo>) {
Object.assign(selfInfo, data)
}
export function getSelfUid() {
return selfInfo['uid']
}
export function getSelfUin() {
return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
let expire: number
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
expire ??= getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) {
return
}
const id = msg.msgId
messages.set(id, msg)
setTimeout(() => {
messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
export function getMsgCache(msgId: string) {
return messages.get(msgId)
}

View File

@@ -1,275 +0,0 @@
import { Level } from 'level'
import { type GroupNotify, RawMessage } from '../ntqqapi/types'
import { DATA_DIR } from './utils'
import { selfInfo } from './data'
import { FileCache } from './types'
import { log } from './utils/log'
type ReceiveTempUinMap = Record<string, string>
class DBUtil {
public readonly DB_KEY_PREFIX_MSG_ID = 'msg_id_'
public readonly DB_KEY_PREFIX_MSG_SHORT_ID = 'msg_short_id_'
public readonly DB_KEY_PREFIX_MSG_SEQ_ID = 'msg_seq_id_'
public readonly DB_KEY_PREFIX_FILE = 'file_'
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = 'group_notify_'
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = 'received_temp_uin_map'
public db: Level | undefined
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number | undefined
/*
* 数据库结构
* msg_id_101231230999: {} // 长id: RawMessage
* msg_short_id_1: 101231230999 // 短id: 长id
* msg_seq_id_1: 101231230999 // 序列id: 长id
* file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */
constructor() {
let initCount = 0
new Promise((resolve, reject) => {
const initDB = () => {
initCount++
// if (initCount > 50) {
// return reject("init db fail")
// }
try {
if (!selfInfo.uin) {
setTimeout(initDB, 300)
return
}
const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}`
this.db = new Level(DB_PATH, { valueEncoding: 'json' })
console.log('llonebot init db success')
resolve(null)
} catch (e: any) {
console.log('init db fail', e.stack.toString())
setTimeout(initDB, 300)
}
}
setTimeout(initDB)
}).then()
const expiredMilliSecond = 1000 * 60 * 60
setInterval(() => {
// this.cache = {}
// 清理时间较久的缓存
const now = Date.now()
for (let key in this.cache) {
let message: RawMessage = this.cache[key] as RawMessage
if (message?.msgTime) {
if (now - parseInt(message.msgTime) * 1000 > expiredMilliSecond) {
delete this.cache[key]
// log("clear cache", key, message.msgTime);
}
}
}
}, expiredMilliSecond)
}
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db?.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)!)
} catch (e) { }
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data
this.db?.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
this.cache[longIdKey] = this.cache[shortIdKey] = msg
}
public clearCache() {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage | undefined> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try {
const longId = await this.db?.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e: any) {
log('getMsgByShortId db error', e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage | undefined> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db?.get(longIdKey)
const msg = JSON.parse(data!)
this.addCache(msg)
return msg
} catch (e) {
// log("getMsgByLongId db error", e.stack.toString())
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage | undefined> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db?.get(seqIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e: any) {
log('getMsgBySeqId db error', e.stack.toString())
}
}
async addMsg(msg: RawMessage) {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
// log("addMsg getMsgByLongId error", e.stack.toString())
}
}
if (existMsg) {
// log("消息已存在", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId)
this.updateMsg(msg).then()
return existMsg.msgShortId
}
const shortMsgId = await this.genMsgShortId()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
msg.msgShortId = shortMsgId
this.addCache(msg)
// log("新增消息记录", msg.msgId)
this.db?.put(shortIdKey, msg.msgId).then().catch()
this.db?.put(longIdKey, JSON.stringify(msg)).then().catch()
try {
await this.db?.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db?.put(seqIdKey, msg.msgId).then().catch()
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg
}
return shortMsgId
// log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`);
}
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
existMsg = msg
}
}
Object.assign(existMsg!, msg)
this.db?.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg?.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg!
}
this.db?.put(shortIdKey, msg.msgId).then().catch()
try {
await this.db?.get(seqIdKey)
} catch (e) {
this.db?.put(seqIdKey, msg.msgId).then().catch()
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
}
private async genMsgShortId(): Promise<number> {
const key = 'msg_current_short_id'
if (this.currentShortId === undefined) {
try {
const id = await this.db?.get(key)
this.currentShortId = parseInt(id!)
} catch (e) {
this.currentShortId = -2147483640
}
}
this.currentShortId++
this.db?.put(key, this.currentShortId.toString()).then().catch()
return this.currentShortId
}
async addFileCache(fileNameOrUuid: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) {
return
}
let cacheDBData = { ...data }
delete cacheDBData['downloadFunc']
this.cache[fileNameOrUuid] = data
try {
await this.db?.put(key, JSON.stringify(cacheDBData))
} catch (e: any) {
log('addFileCache db error', e.stack.toString())
}
}
async getFileCache(fileNameOrUuid: string): Promise<FileCache | undefined> {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) {
return this.cache[key] as FileCache
}
try {
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getFileCache db error", e.stack.toString())
}
}
async addGroupNotify(notify: GroupNotify) {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + notify.seq
let existNotify = this.cache[key] as GroupNotify
if (existNotify) {
return
}
this.cache[key] = notify
this.db?.put(key, JSON.stringify(notify)).then().catch()
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + seq
if (this.cache[key]) {
return this.cache[key] as GroupNotify
}
try {
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}
}
}
export const dbUtil = new DBUtil()

View File

@@ -58,7 +58,7 @@ export abstract class HttpServerBase {
start(port: number) { start(port: number) {
try { try {
this.expressAPP.get('/', (req: Request, res: Response) => { this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`${this.name}已启动`) res.send(`${this.name} 已启动`)
}) })
this.listen(port) this.listen(port)
llonebotError.httpServerError = '' llonebotError.httpServerError = ''

View File

@@ -30,6 +30,8 @@ export interface Config {
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
musicSignUrl?: string musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */
msgCacheExpire?: number
} }
export interface LLOneBotError { export interface LLOneBotError {
@@ -41,11 +43,22 @@ export interface LLOneBotError {
export interface FileCache { export interface FileCache {
fileName: string fileName: string
filePath: string
fileSize: string fileSize: string
fileUuid?: string msgId: string
url?: string peerUid: string
msgId?: string chatType: number
elementId: string elementId: string
downloadFunc?: () => Promise<void> 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

@@ -16,6 +16,7 @@ export interface ListenerIBase {
new(listener: any): ListenerClassBase new(listener: any): ListenerClassBase
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20
export class NTEventWrapper { export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数 private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
@@ -33,7 +34,7 @@ export class NTEventWrapper {
if (typeof target[prop] === 'undefined') { if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod // 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => { return (...args: any[]) => {
current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then() current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
} }
} }
// 如果方法存在,正常返回 // 如果方法存在,正常返回
@@ -47,7 +48,7 @@ export class NTEventWrapper {
this.WrapperSession = WrapperSession this.WrapperSession = WrapperSession
} }
CreatEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined { createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/') const eventNameArr = eventName.split('/')
type eventType = { type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> } [key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
@@ -68,14 +69,14 @@ export class NTEventWrapper {
} }
} }
CreatListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T { createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName] const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode) let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) { if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName)) Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1] const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener' const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.CreatEventFunction<(listener: T) => number>(Service) const addfunc = this.createEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T) addfunc!(Listener as T)
//console.log(addfunc!(Listener as T)) //console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener) this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
@@ -84,7 +85,7 @@ export class NTEventWrapper {
} }
//统一回调清理事件 //统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) { async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args) //console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => { this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout) //console.log(task.func, uuid, task.createtime, task.timeout)
@@ -100,7 +101,7 @@ export class NTEventWrapper {
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) { async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => { return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.CreatEventFunction<EventType>(EventName) const EventFunc = this.createEventFunction<EventType>(EventName)
let complete = false let complete = false
const Timeouter = setTimeout(() => { const Timeouter = setTimeout(() => {
if (!complete) { if (!complete) {
@@ -149,7 +150,7 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()) this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
} }
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak) this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName) this.createListenerFunction(ListenerMainName)
}) })
} }
@@ -195,8 +196,8 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map()) this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
} }
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak) this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName) this.createListenerFunction(ListenerMainName)
const EventFunc = this.CreatEventFunction<EventType>(EventName) const EventFunc = this.createEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...(args as any[])) retEvent = await EventFunc!(...(args as any[]))
}) })
} }

View File

@@ -0,0 +1,163 @@
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from './table'
import { DATA_DIR } from './index'
import Database, { Tables } from 'minato'
import SQLite from '@minatojs/driver-sqlite'
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import { FileCacheV2 } from '../types'
interface SQLiteTables extends Tables {
message: {
shortId: number
msgId: string
chatType: number
peerUid: string
}
file_v2: FileCacheV2
}
interface MsgIdAndPeerByShortId {
MsgId: string
Peer: Peer
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84
class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number>
private msgIdMap: LimitedHashTable<string, number>
private db: Database<SQLiteTables> | undefined
constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap)
this.msgDataMap = new LimitedHashTable<string, number>(maxMap)
}
async init(uin: string) {
const dbDir = path.join(DATA_DIR, 'database')
if (!fs.existsSync(dbDir)) {
await fsPromise.mkdir(dbDir)
}
const database = new Database<SQLiteTables>()
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
this.db = database
}
async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> {
const heads = this.msgIdMap.getHeads(size)
if (!heads) {
return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number | undefined {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> {
const data = this.msgDataMap.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId)
}
async getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId)
if (!shortId) return undefined
return await this.getMsgIdAndPeerByShortId(shortId)
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize)
this.msgDataMap.resize(maxSize)
}
addFileCache(data: FileCacheV2) {
return this.db?.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@@ -1,5 +1,4 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs'
import os from 'node:os' import os from 'node:os'
import { systemPlatform } from './system' import { systemPlatform } from './system'
@@ -33,37 +32,11 @@ if (typeof configVersionInfoPath !== 'string') {
export { configVersionInfoPath } export { configVersionInfoPath }
type QQPkgInfo = { type QQPkgInfo = {
version: string; version: string
buildVersion: string; buildVersion: string
platform: string; platform: string
eleArch: string; eleArch: string
} }
type QQVersionConfigInfo = {
baseVersion: string;
curVersion: string;
prevVersion: string;
onErrorVersions: Array<any>;
buildId: string;
}
let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.9-23361',
'curVersion': '9.9.9-23361',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '23361',
}
if (fs.existsSync(configVersionInfoPath)) {
try {
const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString())
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _)
} catch (e) {
console.error('Load QQ version config info failed, Use default version', e)
}
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath) export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3, // platform_type: 3,
@@ -74,10 +47,6 @@ export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platVer: '10.0.26100', // platVer: '10.0.26100',
// clientVer: '9.9.9-23159', // clientVer: '9.9.9-23159',
let _appid: string = '537213803' // 默认为 Windows 平台的 appid export function getBuildVersion(): number {
if (systemPlatform === 'linux') { return +qqPkgInfo.buildVersion
_appid = '537213827'
} }
// todo: mac 平台的 appid
export const appid = _appid
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

View File

@@ -1,124 +1,93 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log'
import path from 'node:path' import path from 'node:path'
import ffmpeg from 'fluent-ffmpeg'
import fsPromise from 'node:fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm'
import { log } from './log'
import { TEMP_DIR } from './index' import { TEMP_DIR } from './index'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Readable } from 'node:stream'
interface FFmpegOptions {
input?: string[]
output?: string[]
}
type Input = string | Readable
function convert(input: Input, options: FFmpegOptions): Promise<Buffer>
function convert(input: Input, options: FFmpegOptions, outputPath: string): Promise<string>
function convert(input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer> | Promise<string> {
return new Promise<any>((resolve, reject) => {
const chunks: Buffer[] = []
let command = ffmpeg(input)
.on('error', err => {
log(`FFmpeg处理转换出错: `, err.message)
reject(err)
})
.on('end', () => {
if (!outputPath) {
resolve(Buffer.concat(chunks))
} else {
resolve(outputPath)
}
})
if (options.input) {
command = command.inputOptions(options.input)
}
if (options.output) {
command = command.outputOptions(options.output)
}
const ffmpegPath = getConfigUtil().getConfig().ffmpeg
if (ffmpegPath) {
command = command.setFfmpegPath(ffmpegPath)
}
if (!outputPath) {
const stream = command.pipe()
stream.on('data', chunk => {
chunks.push(chunk)
})
} else {
command.save(outputPath)
}
})
}
export async function encodeSilk(filePath: string) { export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: 'r',
})
const fileHeader = buffer.toString('hex', 0, bytesToRead)
return fileHeader
} catch (err) {
console.error('读取文件错误:', err)
return
}
}
async function isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath))
}
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
// function verifyDuration(oriDuration: number, guessDuration: number) {
// // 单位都是秒
// if (oriDuration - guessDuration > 10) {
// return guessDuration
// }
// oriDuration = Math.max(1, oriDuration)
// return oriDuration
// }
// async function getAudioSampleRate(filePath: string) {
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try { try {
const file = await fsPromise.readFile(filePath) const file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, randomUUID())
if (!isSilk(file)) { if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
const _isWav = isWav(file) let result: EncodeResult
const pcmPath = pttPath + '.pcm' const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
let sampleRate = 0 if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
const convert = () => { result = await encode(file, 0)
return new Promise<Buffer>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000
const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {
})
return resolve(data)
}
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
reject(Error(`FFmpeg处理转换失败`))
})
})
}
let input: Buffer
if (!_isWav) {
input = await convert()
} else { } else {
input = file const input = await convert(filePath, {
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] output: [
const { fmt } = getWavFileInfo(input) '-ar 24000',
// log(`wav文件信息`, fmt) '-ac 1',
if (!allowSampleRate.includes(fmt.sampleRate)) { '-f s16le'
input = await convert() ]
} })
result = await encode(input, 24000)
} }
const silk = await encode(input, sampleRate) const pttPath = path.join(TEMP_DIR, randomUUID())
fs.writeFileSync(pttPath, silk.data) await fsPromise.writeFile(pttPath, result.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration) log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return { return {
converted: true, converted: true,
path: pttPath, path: pttPath,
duration: silk.duration / 1000, duration: result.duration / 1000,
} }
} else { } else {
const silk = file const silk = file
let duration = 0 let duration = 1
try { try {
duration = getDuration(silk) / 1000 duration = getDuration(silk) / 1000
} catch (e: any) { } catch (e: any) {
log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack) log('获取语音文件时长失败, 默认为1秒', filePath, e.stack)
duration = await guessDuration(filePath)
} }
return { return {
converted: false, converted: false,
path: filePath, path: filePath,
@@ -131,40 +100,20 @@ export async function encodeSilk(filePath: string) {
} }
} }
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') { type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data export async function decodeSilk(inputFilePath: string, outFormat: OutFormat = 'mp3') {
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath)) const silk = await fsPromise.readFile(inputFilePath)
const outPCMPath = fileName + '.pcm' const { data } = await decode(silk, 24000)
const outFilePath = fileName + '.' + outFormat const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath))
await fsPromise.writeFile(outPCMPath, data) const outFilePath = tmpPath + `.${outFormat}`
const convert = () => { const pcmFilePath = tmpPath + '.pcm'
return new Promise<string>((resolve, reject) => { await fsPromise.writeFile(pcmFilePath, data)
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg' return convert(pcmFilePath, {
const cp = spawn(ffmpegPath, [ input: [
'-y', '-f s16le',
'-f', 's16le', // PCM format '-ar 24000',
'-ar', '24000', // Sample rate '-ac 1'
'-ac', '1', // Number of audio channels ]
'-i', outPCMPath, }, outFilePath)
outFilePath,
])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
} }

View File

@@ -2,7 +2,6 @@ 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 { log, TEMP_DIR } from './index'
import { dbUtil } from '../db'
import * as fileType from 'file-type' import * as fileType from 'file-type'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
@@ -25,7 +24,7 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
} else if (Date.now() - startTime > timeout) { } else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`)) reject(new Error(`文件不存在: ${path}`))
} else { } else {
setTimeout(check, 100) setTimeout(check, 200)
} }
} }
@@ -187,13 +186,6 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
} else { } else {
filePath = pathname filePath = pathname
} }
} else {
const cache = await dbUtil.getFileCache(uri)
if (cache) {
filePath = cache.filePath
} else {
filePath = uri
}
} }
res.isLocal = true res.isLocal = true

View File

@@ -41,7 +41,7 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
}) })
} }
export function isNull(value: any): value is null | undefined | void { export function isNull(value: unknown) {
return value === undefined || value === null return value === undefined || value === null
} }
@@ -74,24 +74,96 @@ export function wrapText(str: string, maxLength: number): string {
* @returns 处理后缓存或调用原方法的结果 * @returns 处理后缓存或调用原方法的结果
*/ */
export function cacheFunc(ttl: number, customKey: string = '') { export function cacheFunc(ttl: number, customKey: string = '') {
const cache = new Map<string, { expiry: number; value: any }>(); const cache = new Map<string, { expiry: number; value: any }>()
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value; const originalMethod = descriptor.value
const className = target.constructor.name; // 获取类名 const className = target.constructor.name // 获取类名
const methodName = propertyKey; // 获取方法名 const methodName = propertyKey // 获取方法名
descriptor.value = async function (...args: any[]) { descriptor.value = async function (...args: any[]) {
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`; const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`
const cached = cache.get(cacheKey); const cached = cache.get(cacheKey)
if (cached && cached.expiry > Date.now()) { if (cached && cached.expiry > Date.now()) {
return cached.value; return cached.value
} else { } else {
const result = await originalMethod.apply(this, args); const result = await originalMethod.apply(this, args)
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl }); cache.set(cacheKey, { value: result, expiry: Date.now() + ttl })
return result; return result
} }
}; }
return descriptor; return descriptor
}; }
}
export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
if (!checker(...args, result)) {
return result //丢弃缓存
}
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr)
const low = BigInt(lowStr)
const highHex = high.toString(16).padStart(16, '0')
const lowHex = low.toString(16).padStart(16, '0')
const combinedHex = highHex + lowHex
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`
return uuid
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '')
const high = BigInt('0x' + hex.substring(0, 16))
const low = BigInt('0x' + hex.substring(16))
return { high: high.toString(), low: low.toString() }
}
} }

View File

@@ -1,5 +1,4 @@
import path from 'node:path' import path from 'node:path'
import fs from 'fs'
export * from './file' export * from './file'
export * from './helper' export * from './helper'
@@ -7,13 +6,9 @@ export * from './log'
export * from './qqlevel' export * from './qqlevel'
export * from './QQBasicInfo' export * from './QQBasicInfo'
export * from './upgrade' export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR = path.join(DATA_DIR, 'temp') export const TEMP_DIR: string = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR = global.LiteLoader.plugins['LLOneBot'].path.plugin export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
export { getVideoInfo } from './video' export { getVideoInfo } from './video'
export { checkFfmpeg } from './video' export { checkFfmpeg } from './video'
export { encodeSilk } from './audio' export { encodeSilk } from './audio'
export { isQQ998 } from './QQBasicInfo'

View File

@@ -1,4 +1,4 @@
import { selfInfo } from '../data' import { getSelfInfo } from '../data'
import fs from 'fs' import fs from 'fs'
import path from 'node:path' import path from 'node:path'
import { DATA_DIR, truncateString } from './index' import { DATA_DIR, truncateString } from './index'
@@ -15,7 +15,7 @@ export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) { if (!getConfigUtil().getConfig().log) {
return //console.log(...msg); return //console.log(...msg);
} }
const selfInfo = getSelfInfo()
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : '' const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ''
let logMsg = '' let logMsg = ''
for (let msgItem of msg) { for (let msgItem of msg) {
@@ -31,5 +31,5 @@ export function log(...msg: any[]) {
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg); // sendLog(...msg);
// console.log(msg) // console.log(msg)
fs.appendFile(path.join(logDir, logFileName), logMsg, (err: any) => {}) fs.appendFile(path.join(logDir, logFileName), logMsg, () => {})
} }

72
src/common/utils/table.ts Normal file
View File

@@ -0,0 +1,72 @@
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5
export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map()
private valueToKey: Map<V, K> = new Map()
private maxSize: number
constructor(maxSize: number) {
this.maxSize = maxSize
}
resize(count: number) {
this.maxSize = count
}
set(key: K, value: V): void {
this.keyToValue.set(key, value)
this.valueToKey.set(value, key)
while (this.keyToValue.size !== this.valueToKey.size) {
console.log('keyToValue.size !== valueToKey.size Error Atom')
this.keyToValue.clear()
this.valueToKey.clear()
}
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value
this.valueToKey.delete(this.keyToValue.get(oldestKey)!)
this.keyToValue.delete(oldestKey)
}
}
getValue(key: K): V | undefined {
return this.keyToValue.get(key)
}
getKey(value: V): K | undefined {
return this.valueToKey.get(value)
}
deleteByValue(value: V): void {
const key = this.valueToKey.get(value)
if (key !== undefined) {
this.keyToValue.delete(key)
this.valueToKey.delete(value)
}
}
deleteByKey(key: K): void {
const value = this.keyToValue.get(key)
if (value !== undefined) {
this.keyToValue.delete(key)
this.valueToKey.delete(value)
}
}
getKeyList(): K[] {
return Array.from(this.keyToValue.keys())
}
//获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined {
const keyList = this.getKeyList()
if (keyList.length === 0) {
return undefined
}
const result: { key: K; value: V }[] = []
const listSize = Math.min(size, keyList.length)
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]
result.push({ key, value: this.keyToValue.get(key)! })
}
return result
}
}

View File

@@ -1,7 +1,8 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import * as fs from 'node:fs' import path from 'node:path'
import fs from 'node:fs'
import { Config } from '../common/types' import { Config } from '../common/types'
import { import {
CHANNEL_CHECK_VERSION, CHANNEL_CHECK_VERSION,
@@ -13,53 +14,46 @@ 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, qqPkgInfo } from '../common/utils' import { DATA_DIR, TEMP_DIR } from '../common/utils'
import { import {
friendRequests,
getFriend,
getGroup,
getGroupMember,
groups,
llonebotError, llonebotError,
refreshGroupMembers, setSelfInfo,
selfInfo, getSelfInfo,
uidMaps, getSelfUid,
getSelfUin,
addMsgCache
} from '../common/data' } from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook' import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { import {
ChatType,
FriendRequestNotify, FriendRequestNotify,
GroupMemberRole, GroupNotify,
GroupNotifies,
GroupNotifyTypes, GroupNotifyTypes,
RawMessage, RawMessage,
BuddyReqType,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http' import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket' import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import * as path from 'node:path' import { MessageUnique } from '../common/utils/MessageUnique'
import { dbUtil } from '../common/db'
import { setConfig } from './setConfig' import { setConfig } from './setConfig'
import { NTQQUserApi } from '../ntqqapi/api/user' import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/wrapper' import '../ntqqapi/wrapper'
import { sentMessages } from '@/ntqqapi/api'
import { NTEventDispatch } from '../common/utils/EventTask' import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperApi, wrapperConstructor } from '../ntqqapi/wrapper' import { wrapperConstructor, getSession } from '../ntqqapi/wrapper'
import { Peer } from '../ntqqapi/types'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log('llonebot main onLoad')
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => { ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkNewVersion() return checkNewVersion()
}) })
@@ -101,7 +95,7 @@ function onLoad() {
} }
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常' llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常'
let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace('\n\n', '\n') error = error.replace('\n\n', '\n')
@@ -160,16 +154,19 @@ function onLoad() {
continue continue
} }
// log("收到新消息", message.msgId, message.msgSeq) // log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){ const peer: Peer = {
message.msgShortId = await dbUtil.addMsg(message) chatType: message.chatType,
// } peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
addMsgCache(message)
OB11Constructor.message(message) OB11Constructor.message(message)
.then((msg) => { .then((msg) => {
if (!debug && msg.message.length === 0) { if (!debug && msg.message.length === 0) {
return return
} }
const isSelfMsg = msg.user_id.toString() == selfInfo.uin const isSelfMsg = msg.user_id.toString() === getSelfUin()
if (isSelfMsg && !reportSelfMessage) { if (isSelfMsg && !reportSelfMessage) {
return return
} }
@@ -187,7 +184,7 @@ function onLoad() {
} }
}) })
OB11Constructor.PrivateEvent(message).then((privateEvent) => { OB11Constructor.PrivateEvent(message).then((privateEvent) => {
log(message) //log(message)
if (privateEvent) { if (privateEvent) {
// log("post private event", privateEvent); // log("post private event", privateEvent);
postOb11Event(privateEvent) postOb11Event(privateEvent)
@@ -217,33 +214,22 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报 const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
const sentMessage = sentMessages[message.msgId]
if (sentMessage) {
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message)
if (message.recallTime != '0') { if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) { if (recallMsgIds.includes(message.msgId)) {
continue continue
} }
recallMsgIds.push(message.msgId) recallMsgIds.push(message.msgId)
const oriMessage = await dbUtil.getMsgByLongId(message.msgId) const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId)
if (!oriMessage) { if (!oriMessageId) {
continue continue
} }
oriMessage.recallTime = message.recallTime OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => {
dbUtil.updateMsg(oriMessage).then()
message.msgShortId = oriMessage.msgShortId
OB11Constructor.RecallEvent(message).then((recallEvent) => {
if (recallEvent) { if (recallEvent) {
log('post recall event', recallEvent) //log('post recall event', recallEvent)
postOb11Event(recallEvent) postOb11Event(recallEvent)
} }
}) })
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
} }
dbUtil.updateMsg(message).then()
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
@@ -258,6 +244,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
@@ -265,131 +252,87 @@ 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
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`); const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
// if (notifyTime < startTime) { if (notifyTime < startTime || processedGroupNotify.includes(flag)) {
// continue;
// }
let existNotify = await dbUtil.getGroupNotify(notify.seq)
if (existNotify) {
continue continue
} }
log('收到群通知', notify) processedGroupNotify.push(flag)
await dbUtil.addGroupNotify(notify)
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
// 原本的群管变更通知事件处理
// if (
// [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
// notify.type,
// )
// ) {
// const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
// log('有管理员变动通知')
// refreshGroupMembers(notify.group.groupCode).then()
// let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
// groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
// log('开始获取变动的管理员')
// if (member1) {
// log('变动管理员获取成功')
// groupAdminNoticeEvent.user_id = parseInt(member1.uin)
// groupAdminNoticeEvent.sub_type = [
// GroupNotifyTypes.ADMIN_UNSET,
// GroupNotifyTypes.ADMIN_UNSET_OTHER,
// ].includes(notify.type)
// ? 'unset'
// : 'set'
// // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
// postOb11Event(groupAdminNoticeEvent, true)
// }
// else {
// log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
// }
// }
// else
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) { if (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('有加群请求')
let requestQQ = uidMaps[notify.user1.uid] let requestQQ = ''
if (!requestQQ) { try {
try { // uid-->uin
requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid))
if (isNaN(parseInt(requestQQ))) {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
} }
} catch (e) {
log('获取加群人QQ号失败 Uid:', notify.user1.uid, e)
} }
let invitorId: number let invitorId: string
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) { if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite' // groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid] try {
if (!invitorQQ) { // uid-->uin
try { invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid))
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)) if (isNaN(parseInt(invitorId))) {
invitorId = parseInt(invitor.uin) invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin
} catch (e) {
invitorId = 0
log('获取邀请人QQ号失败', e)
} }
} catch (e) {
invitorId = ''
log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e)
} }
} }
const groupRequestEvent = new OB11GroupRequestEvent( const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0, parseInt(requestQQ) || 0,
notify.seq, flag,
notify.postscript, notify.postscript,
invitorId!, invitorId! === undefined ? undefined : +invitorId,
'add' 'add'
) )
postOb11Event(groupRequestEvent) postOb11Event(groupRequestEvent)
} }
else if (notify.type == GroupNotifyTypes.INVITE_ME) { else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知') log('收到邀请我加群通知')
let userId = uidMaps[notify.user2.uid] const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || ''
if (!userId) {
userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
const groupInviteEvent = new OB11GroupRequestEvent( const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(userId), parseInt(userId),
notify.seq, flag,
undefined, undefined,
undefined, undefined,
'invite' 'invite'
@@ -408,67 +351,51 @@ function onLoad() {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) { for (const req of payload.data.buddyReqs) {
const flag = req.friendUid + req.reqTime if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) { continue
friendRequests[flag] = req
log('有新的好友请求', req)
let userId: number
try {
const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
userId = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag)
postOb11Event(friendRequestEvent)
} }
let userId = 0
try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin!)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
postOb11Event(friendRequestEvent)
} }
}) })
} }
let startTime = 0 // 毫秒 let startTime = 0 // 毫秒
async function start() { async function start(uid: string, uin: string) {
log('llonebot pid', process.pid) log('llonebot pid', process.pid)
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
if (!config.enableLLOB) { if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot') log('LLOneBot 开关设置为关闭不启动LLOneBot')
return return
} }
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
llonebotError.otherError = '' llonebotError.otherError = ''
startTime = Date.now() startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => { NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! })
for (const [key, value] of Object.entries(m)) { MessageUnique.init(uin)
uidMaps[value] = key
}
})
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: wrapperApi.NodeIQQNTWrapperSession! })
try {
log('start get groups')
const _groups = await NTQQGroupApi.getGroups()
log('_groups', _groups)
await Promise.all(
_groups.map(async (group) => {
try {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = members
groups.push(group)
} catch (e) {
log('获取群成员失败', e)
}
})
)
}
catch (e) {
log('获取群列表失败', e)
}
finally {
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
}
log('start activate group member info')
// 下面两个会导致CPU占用过高QQ卡死
// NTQQGroupApi.activateMemberInfoChange().then().catch(log)
// NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
if (config.ob11.enableHttp) { if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort) ob11HTTPServer.start(config.ob11.httpPort)
@@ -486,54 +413,25 @@ function onLoad() {
log('LLOneBot start') log('LLOneBot start')
} }
let getSelfNickCount = 0 const intervalId = setInterval(() => {
const init = async () => { const current = getSelfInfo()
try { if (!current.uin) {
log('start get self info') setSelfInfo({
const _ = await NTQQUserApi.getSelfInfo() uin: globalThis.authData?.uin,
log('get self info api result:', _) uid: globalThis.authData?.uid,
Object.assign(selfInfo, _) nick: current.uin,
selfInfo.nick = selfInfo.uin })
} catch (e) {
log('retry get self info', e)
} }
if (!selfInfo.uin) { if (current.uin && getSession()) {
selfInfo.uin = globalThis.authData?.uin clearInterval(intervalId)
selfInfo.uid = globalThis.authData?.uid start(current.uid, current.uin)
selfInfo.nick = selfInfo.uin
} }
log('self info', selfInfo, globalThis.authData) }, 600)
if (selfInfo.uin) {
async function getUserNick() {
try {
getSelfNickCount++
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
log('self info', userInfo)
if (userInfo) {
selfInfo.nick = userInfo.nick
return
}
} catch (e: any) {
log('get self nickname failed', e.stack)
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000)
}
}
getUserNick().then()
start().then()
}
else {
setTimeout(init, 1000)
}
}
setTimeout(init, 1000)
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) { if (getSelfUid()) {
return return
} }
mainWindow = window mainWindow = window

View File

@@ -9,40 +9,35 @@ import {
ChatType, ChatType,
ElementType, ElementType,
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement, IMAGE_HTTP_HOST_NT,
PicElement,
} from '../types' } from '../types'
import path from 'node:path' 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 } from '@/common/utils' import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey' import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } 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 { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { NodeIKernelSearchService } from '@/ntqqapi/services'
export class NTQQFileApi { export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> { static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
const session = wrapperApi.NodeIQQNTWrapperSession 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 await callNTQQApi<{ ext: string }>({ return fileTypeFromFile(filePath)
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_TYPE,
args: [filePath],
})
}
static async getFileMd5(filePath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath],
})
} }
static async copyFile(filePath: string, destPath: string) { static async copyFile(filePath: string, destPath: string) {
@@ -67,44 +62,35 @@ export class NTQQFileApi {
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath) const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext let ext = (await NTQQFileApi.getFileType(filePath))?.ext || ''
if (ext) { if (ext) {
ext = '.' + ext ext = '.' + ext
} else {
ext = ''
} }
let fileName = `${path.basename(filePath)}` let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) { if (fileName.indexOf('.') === -1) {
fileName += ext fileName += ext
} }
const mediaPath = await callNTQQApi<string>({ const session = getSession()
methodName: NTQQApiMethod.MEDIA_FILE_PATH, const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
args: [ md5HexStr: fileMd5,
{ fileName: fileName,
path_info: { elementType: elementType,
md5HexStr: md5, elementSubType,
fileName: fileName, thumbSize: 0,
elementType: elementType, needCreate: true,
elementSubType, downloadType: 1,
thumbSize: 0, file_uuid: ''
needCreate: true,
downloadType: 1,
file_uuid: '',
},
},
],
}) })
log('media path', mediaPath) await fsPromise.copyFile(filePath, mediaPath!)
await NTQQFileApi.copyFile(filePath, mediaPath) const fileSize = (await fsPromise.stat(filePath)).size
const fileSize = await NTQQFileApi.getFileSize(filePath)
return { return {
md5, md5: fileMd5,
fileName, fileName,
path: mediaPath, path: mediaPath!,
fileSize, fileSize,
ext, ext
} }
} }
@@ -115,44 +101,67 @@ export class NTQQFileApi {
elementId: string, elementId: string,
thumbPath: string, thumbPath: string,
sourcePath: string, sourcePath: string,
force: boolean = false, timeout = 1000 * 60 * 2,
force = false
) { ) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
if (force) { if (force) {
fs.unlinkSync(sourcePath) try {
await fsPromise.unlink(sourcePath)
} catch (e) {
//
}
} else { } else {
return sourcePath return sourcePath
} }
} }
const apiParams = [ const data = await NTEventDispatch.CallNormalEvent<
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{ {
getReq: { fileModelId: '0',
fileModelId: '0', downloadSourceType: 0,
downloadSourceType: 0, triggerType: 1,
triggerType: 1, msgId: msgId,
msgId: msgId, chatType: chatType,
chatType: chatType, peerUid: peerUid,
peerUid: peerUid, elementId: elementId,
elementId: elementId, thumbSize: 0,
thumbSize: 0, downloadType: 1,
downloadType: 1, filePath: thumbPath
filePath: thumbPath, }
}, )
}, let filePath = data[1].filePath
null, if (filePath.startsWith('\\')) {
] const downloadPath = TEMP_DIR
// log("需要下载media", sourcePath); filePath = path.join(downloadPath, filePath)
await callNTQQApi({ // 下载路径是下载文件夹的相对路径
methodName: NTQQApiMethod.DOWNLOAD_MEDIA, }
args: apiParams, return filePath
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => {
log('media 下载完成判断', payload.notifyInfo.msgId, msgId)
return payload.notifyInfo.msgId == msgId
},
})
return sourcePath
} }
static async getImageSize(filePath: string) { static async getImageSize(filePath: string) {
@@ -163,22 +172,26 @@ export class NTQQFileApi {
}) })
} }
static async getImageUrl(picElement: PicElement, chatType: ChatType) { static async getImageUrl(element: PicElement) {
const isPrivateImage = chatType !== ChatType.group if (!element) {
const url = picElement.originImageUrl // 没有域名 return ''
const md5HexStr = picElement.md5HexStr }
const fileMd5 = picElement.md5HexStr const url: string = element.originImageUrl! // 没有域名
const fileUuid = picElement.fileUuid const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr
if (url) { if (url) {
if (url.startsWith('/download')) { const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
// console.log('rkey', rkey); const imageAppid = UrlParse.searchParams.get('appid')
if (url.includes('&rkey=')) { const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey')
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
} }
const rkeyData = await rkeyManager.getRkey()
const rkeyData = await rkeyManager.getRkey(); UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey; return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
} else { } else {
// 老的图片url不需要rkey // 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url return IMAGE_HTTP_HOST + url
@@ -187,9 +200,125 @@ export class NTQQFileApi {
// 没有url需要自己拼接 // 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
} }
log('图片url获取失败', picElement) 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 {

View File

@@ -1,12 +1,14 @@
import { Friend, FriendRequest, FriendV2 } from '../types' import { Friend, FriendV2 } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { friendRequests } from '../../common/data' import { getSession } from '@/ntqqapi/wrapper'
import { wrapperApi } 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'
export class NTQQFriendApi { export class NTQQFriendApi {
/** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { data: {
@@ -47,29 +49,23 @@ export class NTQQFriendApi {
} }
static async handleFriendRequest(flag: string, accept: boolean) { static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag] const data = flag.split('|')
if (!request) { if (data.length < 2) {
throw `flat: ${flag}, 对应的好友请求不存在` return
} }
const result = await callNTQQApi<GeneralCallResult>({ const friendUid = data[0]
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, const reqTime = data[1]
args: [ const session = getSession()
{ return session?.getBuddyService().approvalFriendRequest({
approvalInfo: { friendUid,
friendUid: request.friendUid, reqTime,
reqTime: request.reqTime, accept
accept,
},
},
],
}) })
delete friendRequests[flag]
return result
} }
static async getBuddyV2(refresh = false): Promise<FriendV2[]> { static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = [] const uids: string[] = []
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
const buddyService = session?.getBuddyService() const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!) uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!)
@@ -78,4 +74,53 @@ export class NTQQFriendApi {
) )
return Array.from(data.values()) return Array.from(data.values())
} }
@CacheClassFuncAsyncExtend(3600 * 1000, 'getBuddyIdMap', () => true)
static async getBuddyIdMapCache(refresh = false): Promise<LimitedHashTable<string, string>> {
return await NTQQFriendApi.getBuddyIdMap(refresh)
}
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const uids: string[] = []
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000)
const session = getSession()
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
);
data.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
//console.log('getBuddyIdMap', retMap.getValue)
return retMap
}
static async getBuddyV2ExWithCate(refresh = false) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const session = getSession()
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data : (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data
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
})
}
static async isBuddy(uid: string): Promise<boolean> {
const session = getSession()
return session?.getBuddyService().isBuddy(uid)!
}
} }

View File

@@ -1,11 +1,11 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { deleteGroup, uidMaps } from '../../common/data'
import { dbUtil } from '../../common/db'
import { log } from '../../common/utils/log'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindowApi, NTQQWindows } from './window'
import { wrapperApi } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services'
export class NTQQGroupApi { export class NTQQGroupApi {
static async activateMemberListChange() { static async activateMemberListChange() {
@@ -37,63 +37,47 @@ export class NTQQGroupApi {
}) })
} }
static async getGroups(forced = false) { static async getGroups(forced = false): Promise<Group[]> {
// let cbCmd = ReceiveCmdS.GROUPS type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']
// if (process.platform != 'win32') { const [, , groupList] = await NTEventDispatch.CallNormalEvent
// cbCmd = ReceiveCmdS.GROUPS_STORE <(force: boolean) => Promise<any>, ListenerType>
// } (
const result = await callNTQQApi<{ 'NodeIKernelGroupService/getGroupList',
updateType: number 'NodeIKernelGroupListener/onGroupListUpdate',
groupList: Group[] 1,
}>({ 5000,
methodName: NTQQApiMethod.GROUPS, () => true,
args: [{ force_update: forced }, undefined], forced
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE], )
afterFirstCmd: false, return groupList
})
log('get groups result', result)
return result.groupList
} }
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> { static async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
const sceneId = await callNTQQApi({ type ListenerType = NodeIKernelGroupListener['onMemberInfoChange']
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, type EventType = NodeIKernelGroupService['getMemberInfo']
args: [ const [, , , _members] = await NTEventDispatch.CallNormalEvent<EventType, ListenerType>
{ (
groupCode: groupQQ, 'NodeIKernelGroupService/getMemberInfo',
scene: 'groupMemberList_MainWindow', 'NodeIKernelGroupListener/onMemberInfoChange',
1,
5000,
(groupCode: string, changeType: number, members: Map<string, GroupMember>) => {
return groupCode == GroupCode && members.has(uid)
}, },
], GroupCode, [uid], forced,
}) )
// log("get group member sceneId", sceneId) return _members.get(uid)
try { }
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [
{
sceneId: sceneId,
num: num,
},
null,
],
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
const values = result.result.infos.values()
const members: GroupMember[] = Array.from(values) static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
for (const member of members) { const session = getSession()
uidMaps[member.uid] = member.uin const groupService = session?.getGroupService()
} const sceneId = groupService?.createMemberListScene(groupQQ, 'groupMemberList_MainWindow')
// log(uidMaps) const result = await groupService?.getNextMemberList(sceneId!, undefined, num)
// log("members info", values) if (result?.errCode !== 0) {
log(`get group ${groupQQ} members success`) throw ('获取群成员列表出错,' + result?.errMsg)
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
return []
} }
return result.result.infos
} }
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) { static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) {
@@ -134,130 +118,94 @@ export class NTQQGroupApi {
) )
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async getSingleScreenNotifies(num: number) {
const notify = await dbUtil.getGroupNotify(seq) const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
if (!notify) { <(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
throw `${seq}对应的加群通知不存在` (
} 'NodeIKernelGroupService/getSingleScreenNotifies',
// delete groupNotifies[seq] 'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
return await callNTQQApi<GeneralCallResult>({ 1,
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, 5000,
args: [ () => true,
{ false,
doubt: false, '',
operateMsg: { num,
operateType: operateType, // 2 拒绝 )
targetMsg: { return notifies
seq: seq, // 通知序列号 }
type: notify.type,
groupCode: notify.group.groupCode, static async delGroupFile(groupCode: string, files: string[]) {
postscript: reason, const session = getSession()
}, return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)!
}, }
},
null, static DelGroupFile = NTQQGroupApi.delGroupFile
],
}) static async delGroupFileFolder(groupCode: string, folderId: string) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFolder(groupCode, folderId)!
}
static DelGroupFileFolder = NTQQGroupApi.delGroupFileFolder
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|')
const groupCode = flagitem[0]
const seq = flagitem[1]
const type = parseInt(flagitem[2])
const session = getSession()
return session?.getGroupService().operateSysNotify(
false,
{
'operateType': operateType, // 2 拒绝
'targetMsg': {
'seq': seq, // 通知序列号
'type': type,
'groupCode': groupCode,
'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}
})
} }
static async quitGroup(groupQQ: string) { static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.QUIT_GROUP, return session?.getGroupService().quitGroup(groupQQ)
args: [{ groupCode: groupQQ }, null],
})
if (result.result === 0) {
deleteGroup(groupQQ)
}
return result
} }
static async kickMember( static async kickMember(
groupQQ: string, groupQQ: string,
kickUids: string[], kickUids: string[],
refuseForever: boolean = false, refuseForever = false,
kickReason: string = '', kickReason = '',
) { ) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.KICK_MEMBER, return session?.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
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为解除禁言
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.MUTE_MEMBER, return session?.getGroupService().setMemberShutUp(groupQQ, memList)
args: [
{
groupCode: groupQQ,
memList,
},
],
})
} }
static async banGroup(groupQQ: string, shutUp: boolean) { static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.MUTE_GROUP, return session?.getGroupService().setGroupShutUp(groupQQ, shutUp)
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
} }
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
NTQQGroupApi.activateMemberListChange().then().catch(log) const session = getSession()
const res = await callNTQQApi<GeneralCallResult>({ return session?.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res
} }
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_MEMBER_ROLE, return session?.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
} }
static async setGroupName(groupQQ: string, groupName: string) { static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_GROUP_NAME, return session?.getGroupService().modifyGroupName(groupQQ, groupName, false)
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
} }
static async getGroupAtAllRemainCount(groupCode: string) { static async getGroupAtAllRemainCount(groupCode: string) {
@@ -282,46 +230,40 @@ export class NTQQGroupApi {
}) })
} }
static async getGroupRemainAtTimes(GroupCode: string) {
const session = getSession()
return session?.getGroupService().getGroupRemainAtTimes(GroupCode)!
}
// 头衔不可用 // 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) { static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title,
},
null,
],
})
} }
static publishGroupBulletin(groupQQ: string, title: string, content: 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 = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
// 代码没测过 // 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false) let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = { let param = {
groupCode: GroupCode, groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgRandom: parseInt(MsgData?.msgList[0].msgRandom!),
msgSeq: parseInt(MsgData.msgList[0].msgSeq) msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
} }
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数 // GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param) return session?.getGroupService().removeGroupEssence(param)
} }
static async addGroupEssence(GroupCode: string, msgId: string) { static async addGroupEssence(GroupCode: string, msgId: string) {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
// 代码没测过 // 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom // 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false) let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = { let param = {
groupCode: GroupCode, groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom), msgRandom: parseInt(MsgData?.msgList[0].msgRandom!),
msgSeq: parseInt(MsgData.msgList[0].msgSeq) msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
} }
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数 // GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param) return session?.getGroupService().addGroupEssence(param)

View File

@@ -1,135 +1,50 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { ChatType, RawMessage, SendMessageElement, Peer } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { dbUtil } from '../../common/db' import { getSelfNick, getSelfUid } from '../../common/data'
import { selfInfo } from '../../common/data' import { getBuildVersion } from '../../common/utils'
import { ReceiveCmdS, registerReceiveHook } from '../hook' import { getSession } from '@/ntqqapi/wrapper'
import { log } from '../../common/utils/log' import { NTEventDispatch } from '@/common/utils/EventTask'
import { sleep } from '../../common/utils/helper'
import { isQQ998 } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/wrapper'
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {} // peerUid: callbackFunc
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage
async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) {
// 等待上一个相同的peer发送完
const peerUid = peer.peerUid
let checkLastSendUsingTime = 0
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw '发送超时'
}
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500)
checkLastSendUsingTime += 500
return await waitLastSend()
}
else {
return
}
}
await waitLastSend()
let sentMessage: RawMessage | null = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
sentMessages[rawMessage.msgId] = rawMessage
}
let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if (sentMessage.sendStatus == 2) {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
}
else {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw '发送超时'
}
await sleep(500)
return await checkSendComplete()
}
return checkSendComplete()
}
export class NTQQMsgApi { export class NTQQMsgApi {
static enterOrExitAIO(peer: Peer, enter: boolean) { static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
return callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO, return session?.getMsgService().getTempChatInfo(chatType, peerUid)!
args: [ }
{
"info_list": [ static async prepareTempChat(toUserUid: string, GroupCode: string, nickname: string) {
{ //By Jadx/Ida Mlikiowa
peer, let TempGameSession = {
"option": enter ? 1 : 2 nickname: '',
} gameAppId: '',
] selfTinyId: '',
}, peerRoleId: '',
{ peerOpenId: '',
"send": true }
}, 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) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的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()
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.EMOJI_LIKE, return session?.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
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) {
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({ const session = getSession()
methodName: NTQQApiMethod.GET_MULTI_MSG, return session?.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)!
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
}
static async getMsgBoxInfo(peer: Peer) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GET_MSG_BOX_INFO,
args: [
{
contacts: [
peer
],
},
null,
],
})
} }
static async activateChat(peer: Peer) { static async activateChat(peer: Peer) {
@@ -151,145 +66,228 @@ export class NTQQMsgApi {
}) })
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number) { static async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
// 消息时间从旧到新 if (!peer) throw new Error('peer is not allowed')
return await callNTQQApi<GeneralCallResult & { msgList: RawMessage[] }>({ if (!msgIds) throw new Error('msgIds is not allowed')
methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG, const session = getSession()
args: [ //Mlikiowa 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得
{ return await session?.getMsgService().getMsgsByMsgId(peer, msgIds)!
peer,
msgId,
cnt: count,
queryOrder: true,
},
null,
],
})
} }
static async fetchRecentContact() { static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
await callNTQQApi({ const session = getSession()
methodName: NTQQApiMethod.RECENT_CONTACT, // 消息时间从旧到新
args: [ return session?.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)!
{
fetchParam: {
anchorPointContact: {
contactId: '',
sortField: '',
pos: 0,
},
relativeMoveCount: 0,
listType: 2, // 1普通消息2群助手内的消息
count: 200,
fetchOld: true,
},
},
],
})
} }
static async recallMsg(peer: Peer, msgIds: string[]) { static async recallMsg(peer: Peer, msgIds: string[]) {
return await callNTQQApi({ const session = getSession()
methodName: NTQQApiMethod.RECALL_MSG, return await session?.getMsgService().recallMsg({
args: [ chatType: peer.chatType,
{ peerUid: peer.peerUid
peer, }, msgIds)
msgIds,
},
null,
],
})
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
const waiter = sendWaiter(peer, waitComplete, timeout) function generateMsgId() {
callNTQQApi({ const timestamp = Math.floor(Date.now() / 1000)
methodName: NTQQApiMethod.SEND_MSG, const random = Math.floor(Math.random() * Math.pow(2, 32))
args: [ const buffer = Buffer.alloc(8)
{ buffer.writeUInt32BE(timestamp, 0)
msgId: '0', buffer.writeUInt32BE(random, 4)
peer, const msgId = BigInt("0x" + buffer.toString('hex')).toString()
msgElements, return msgId
msgAttributeInfos: new Map(), }
}, // 此处有采用Hack方法 利用数据返回正确得到对应消息
null, // 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同
], // 谨慎采用 目前测试暂无问题 Developer.Mlikiowa
}).then() let msgId: string
return await waiter try {
msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime())
} catch (error) {
//if (!napCatCore.session.getMsgService()['generateMsgUniqueId'])
//兜底识别策略V2
msgId = generateMsgId()
}
peer.guildId = msgId
const 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.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
'0',
peer,
msgElements,
new Map()
)
const retMsg = data[1].find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
}
})
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 waiter = sendWaiter(destPeer, true, 10000) const session = getSession()
callNTQQApi<GeneralCallResult>({ return session?.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])!
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
}).then().catch(log)
return await waiter
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const msgInfos = msgIds.map((id) => { const senderShowName = await getSelfNick()
return { msgId: id, senderShowName: selfInfo.nick } const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
}) })
const apiArgs = [ const selfUid = getSelfUid()
{ let data = await NTEventDispatch.CallNormalEvent<
msgInfos, (msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
srcContact: srcPeer, (msgList: RawMessage[]) => void
dstContact: destPeer, >(
commentElements: [], 'NodeIKernelMsgService/multiForwardMsgWithComment',
msgAttributeInfos: new Map(), 'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
}, },
null, msgInfos,
] srcPeer,
return await new Promise<RawMessage>((resolve, reject) => { destPeer,
let complete = false [],
setTimeout(() => { new Map()
if (!complete) { )
reject('转发消息超时') for (let msg of data[1]) {
} const arkElement = msg.elements.find(ele => ele.arkElement)
}, 5000) if (!arkElement) {
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => { continue
const msg = payload.msgRecord }
// 需要判断它是转发的消息,并且识别到是当前转发的这一条 const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
const arkElement = msg.elements.find((ele) => ele.arkElement) if (forwardData.app != 'com.tencent.multimsg') {
if (!arkElement) { continue
// log("收到的不是转发消息") }
return if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) {
} return msg
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) }
if (forwardData.app != 'com.tencent.multimsg') { }
return throw new Error('转发消息超时')
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true
await dbUtil.addMsg(msg)
resolve(msg)
log('转发消息成功:', payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs,
}).then((result) => {
log('转发消息结果:', result, apiArgs)
if (result.result !== 0) {
complete = true
reject('转发消息失败,' + JSON.stringify(result))
}
})
})
} }
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 = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z); return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)!
}
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isIncludeCurrent: true,
pageLimit: count,
})
return ret!
}
static async getSingleMsg(peer: Peer, seq: string) {
const session = getSession()
return await session?.getMsgService().getSingleMsg(peer, seq)!
} }
} }

View File

@@ -1,23 +1,14 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import { Group, SelfInfo, User } from '../types' import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { selfInfo, uidMaps } from '../../common/data' import { friends, groupMembers, getSelfUin } from '@/common/data'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils' import { CacheClassFuncAsync, log, getBuildVersion } from '@/common/utils'
import { wrapperApi } 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 } from '../services'
import { NodeIKernelProfileListener } from '../listeners' import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo' import { NTQQFriendApi } from './friend'
const userInfoCache: Record<string, User> = {} // uid: User
export interface ClientKeyData extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}
export class NTQQUserApi { export class NTQQUserApi {
static async setQQAvatar(filePath: string) { static async setQQAvatar(filePath: string) {
@@ -50,7 +41,7 @@ export class NTQQUserApi {
return result.profiles.get(uid) return result.profiles.get(uid)
} }
// 26702 /** 26702 */
static async fetchUserDetailInfo(uid: string) { static async fetchUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo'] type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged'] type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
@@ -61,20 +52,11 @@ export class NTQQUserApi {
'NodeIKernelProfileListener/onUserDetailInfoChanged', 'NodeIKernelProfileListener/onUserDetailInfoChanged',
1, 1,
5000, 5000,
(profile) => { (profile) => profile.uid === uid,
if (profile.uid === uid) {
return true;
}
return false;
},
'BuddyProfileStore', 'BuddyProfileStore',
[ [uid],
uid
],
UserDetailSource.KSERVER, UserDetailSource.KSERVER,
[ [ProfileBizType.KALL]
ProfileBizType.KALL
]
) )
const RetUser: User = { const RetUser: User = {
...profile.simpleInfo.coreInfo, ...profile.simpleInfo.coreInfo,
@@ -89,45 +71,23 @@ export class NTQQUserApi {
} }
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
if (+qqPkgInfo.buildVersion >= 26702) { if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid) return NTQQUserApi.fetchUserDetailInfo(uid)
} }
// this.getUserInfo(uid) type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']
if (!withBizInfo) { const [_retData, profile] = await NTEventDispatch.CallNormalEvent
methodName = NTQQApiMethod.USER_DETAIL_INFO <EventService, EventListener>
} (
const fetchInfo = async () => { 'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
const result = await callNTQQApi<{ info: User }>({ 'NodeIKernelProfileListener/onProfileDetailInfoChanged',
methodName, 2,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO, 5000,
afterFirstCmd: false, (profile) => profile.uid === uid,
cmdCB: (payload) => { uid,
const success = payload.info.uid == uid [0]
// log("get user detail info", success, uid, payload) )
return success return profile
},
args: [
{
uid,
},
null,
],
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
}
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000)
}
const userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
} }
// return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey=' // return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey='
@@ -144,7 +104,8 @@ export class NTQQUserApi {
} }
static async getQzoneCookies() { static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27' 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 } = {} let cookies: { [key: string]: string } = {}
try { try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl) cookies = await RequestUtil.HttpsGetCookies(requestUrl)
@@ -154,32 +115,25 @@ export class NTQQUserApi {
} }
return cookies return cookies
} }
static async getSkey(): Promise<string> { static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey() const clientKeyData = await NTQQUserApi.getClientKey()
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=' + selfInfo.uin const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin()
+ '&clientkey=' + clientKeyData.clientKey + '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return (await RequestUtil.HttpsGetCookies(url))?.skey return (await RequestUtil.HttpsGetCookies(url))?.skey
} }
@cacheFunc(60 * 30 * 1000) @CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) { static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) { const ClientKeyData = await NTQQUserApi.forceFetchClientKey()
let data = (await NTQQUserApi.getQzoneCookies()) const uin = getSelfUin()
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin 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'
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue } const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
} return cookies
const skey = await this.getSkey()
const pskey = (await this.getPSkey([domain])).get(domain)
if (!pskey || !skey) {
throw new Error('获取Cookies失败')
}
const bkn = NTQQUserApi.genBkn(skey)
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`
return { cookies, bkn }
} }
static genBkn(sKey: string) { static genBkn(sKey: string) {
@@ -195,16 +149,151 @@ export class NTQQUserApi {
} }
static async getPSkey(domains: string[]): Promise<Map<string, string>> { static async getPSkey(domains: string[]): Promise<Map<string, string>> {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
const res = await session?.getTipOffService().getPskey(domains, true) const res = await session?.getTipOffService().getPskey(domains, true)
if (res.result !== 0) { if (res?.result !== 0) {
throw new Error(`获取Pskey失败: ${res.errMsg}`) throw new Error(`获取Pskey失败: ${res?.errMsg}`)
} }
return res.domainPskeyMap return res.domainPskeyMap
} }
static async getClientKey(): Promise<ClientKeyData> { static async getClientKey() {
const session = wrapperApi.NodeIQQNTWrapperSession const session = getSession()
return await session?.getTicketService().forceFetchClientKey('') 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) {
const session = getSession()
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
// Uid 好友转
if (!uid) {
friends.forEach((t) => {
if (t.uin == Uin) {
uid = t.uid
}
})
}
//Uid 群友列表转
if (!uid) {
for (let groupMembersList of groupMembers.values()) {
for (let GroupMember of groupMembersList.values()) {
if (GroupMember.uin == Uin) {
uid = GroupMember.uid
}
}
}
}
if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
if (unveifyUid.indexOf('*') == -1) {
uid = unveifyUid
}
}
return uid
}
static async getUidByUinV2(Uin: string) {
const session = getSession()
let uid = (await session?.getProfileService().getUidByUin('FriendsServiceImpl', [Uin]))?.get(Uin)
if (uid) return uid
uid = (await session?.getGroupService().getUidByUins([Uin]))?.uids.get(Uin)
if (uid) return uid
uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
if (uid) return uid
console.log((await NTQQFriendApi.getBuddyIdMapCache(true)))
uid = (await NTQQFriendApi.getBuddyIdMapCache(true)).getValue(Uin)//从Buddy缓存获取Uid
if (uid) return uid
uid = (await NTQQFriendApi.getBuddyIdMap(true)).getValue(Uin)
if (uid) return uid
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(Uin)).detail.uid//从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) uid = unveifyUid
//if (uid) return uid
return uid
}
static async getUidByUin(Uin: string) {
if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUidByUinV2(Uin)
}
return await NTQQUserApi.getUidByUinV1(Uin)
}
static async getUserDetailInfoByUinV2(Uin: string) {
return await NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUinV2>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
)
}
static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
)
}
static async getUinByUidV1(Uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin',
5000,
[Uid]
)
let uin = ret.uinInfo.get(Uid)
if (!uin) {
//从Buddy缓存获取Uin
friends.forEach((t) => {
if (t.uid == Uid) {
uin = t.uin
}
})
}
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
}
return uin
}
static async getUinByUidV2(Uid: string) {
const session = getSession()
let uin = (await session?.getProfileService().getUinByUid('FriendsServiceImpl', [Uid]))?.get(Uid)
if (uin) return uin
uin = (await session?.getGroupService().getUinByUids([Uid]))?.uins.get(Uid)
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) {
if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUinByUidV2(Uid)
}
return await NTQQUserApi.getUinByUidV1(Uid)
}
@CacheClassFuncAsync(3600 * 1000, 'ClientKey')
static async forceFetchClientKey() {
const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')!
} }
} }

View File

@@ -1,7 +1,8 @@
import { WebGroupData, groups, selfInfo } from '@/common/data' 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',
@@ -137,56 +138,44 @@ 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[]> {
log('webapi 获取群成员', GroupCode); //logDebug('webapi 获取群成员', GroupCode)
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>(); let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
try { try {
let cachedData = WebGroupData.GroupData.get(GroupCode); const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com')
let cachedTime = WebGroupData.GroupTime.get(GroupCode); const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const Bkn = WebApi.genBkn(CookiesObject.skey)
if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) { const retList: Promise<WebApiGroupMemberRet>[] = []
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; 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 });
const _Skey = await NTQQUserApi.getSkey(); if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; return []
if (!_Skey || !_Pskey) {
return MemberData;
}
const Bkn = WebApi.genBkn(_Skey);
const retList: Promise<WebApiGroupMemberRet>[] = [];
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 });
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
} else {
for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key]);
}
}
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40);
//遍历批量请求
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 });
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 key in ret.mems) {
MemberData.push(ret.mems[key]);
}
}
WebGroupData.GroupData.set(GroupCode, MemberData);
WebGroupData.GroupTime.set(GroupCode, Date.now());
} else { } else {
MemberData = cachedData as Array<WebApiGroupMember>; for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key])
}
}
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求
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 });
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 key in ret.mems) {
MemberData.push(ret.mems[key])
}
} }
} catch { } catch {
return MemberData; return MemberData
} }
return MemberData; return MemberData
} }
// public static async addGroupDigest(groupCode: string, msgSeq: string) { // 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 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`;
@@ -203,49 +192,47 @@ export class WebApi {
static async setGroupNotice(GroupCode: string, Content: string = '') { static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn} //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} //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 _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']
const _Skey = await NTQQUserApi.getSkey(); const _Skey = await NTQQUserApi.getSkey()
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin()
let ret: any = undefined; let ret: any = undefined
//console.log(CookieValue); //console.log(CookieValue)
if (!_Skey || !_Pskey) { if (!_Skey || !_Pskey) {
//获取Cookies失败 //获取Cookies失败
return undefined; return undefined
} }
const Bkn = WebApi.genBkn(_Skey); 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 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; const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn
try { try {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue }); ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue })
return ret; return ret
} catch (e) { } catch (e) {
return undefined; return undefined
} }
return undefined;
} }
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> { static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']
const _Skey = await NTQQUserApi.getSkey(); const _Skey = await NTQQUserApi.getSkey()
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin()
let ret: WebApiGroupNoticeRet | undefined = undefined; let ret: WebApiGroupNoticeRet | undefined = undefined
//console.log(CookieValue); //console.log(CookieValue)
if (!_Skey || !_Pskey) { if (!_Skey || !_Pskey) {
//获取Cookies失败 //获取Cookies失败
return undefined; return undefined
} }
const Bkn = WebApi.genBkn(_Skey); 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'; 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 { try {
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue }); ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue })
if (ret?.ec !== 0) { if (ret?.ec !== 0) {
return undefined; return undefined
} }
return ret; return ret
} catch (e) { } catch (e) {
return undefined; return undefined
} }
return undefined;
} }
static genBkn(sKey: string) { static genBkn(sKey: string) {

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 './face_config.json'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName export const mFaceCache = new Map<string, string>() // emojiId -> faceName
@@ -44,12 +44,12 @@ export class SendMsgElementConstructor {
} }
} }
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { static at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', elementId: '',
textElement: { textElement: {
content: `@${atName}`, content: display,
atType, atType,
atUid, atUid,
atTinyId: '', atTinyId: '',
@@ -77,7 +77,7 @@ export class SendMsgElementConstructor {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
const maxMB = 30; const maxMB = 30;
if (fileSize > 1024 * 1024 * 30){ if (fileSize > 1024 * 1024 * 30) {
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} }
const imageSize = await NTQQFileApi.getImageSize(picPath) const imageSize = await NTQQFileApi.getImageSize(picPath)
@@ -104,21 +104,21 @@ export class SendMsgElementConstructor {
} }
} }
static async file(filePath: string, fileName: string = ''): Promise<SendFileElement> { static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常,大小为 0'
} }
let element: SendFileElement = { const element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: '', elementId: '',
fileElement: { fileElement: {
fileName: fileName || _fileName, fileName: fileName || _fileName,
filePath: path, folderId: folderId,
filePath: path!,
fileSize: fileSize.toString(), fileSize: fileSize.toString(),
}, },
} }
return element return element
} }
@@ -175,7 +175,6 @@ export class SendMsgElementConstructor {
setTimeout(useDefaultThumb, 5000) setTimeout(useDefaultThumb, 5000)
ffmpeg(filePath) ffmpeg(filePath)
.on('end', () => {})
.on('error', (err) => { .on('error', (err) => {
if (diyThumbPath) { if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath) fs.copyFile(diyThumbPath, thumbPath)
@@ -280,10 +279,10 @@ export class SendMsgElementConstructor {
faceId = parseInt(faceId.toString()) faceId = parseInt(faceId.toString())
// let faceType = parseInt(faceId.toString().substring(0, 1)); // let faceType = parseInt(faceId.toString().substring(0, 1));
let faceType = 1 let faceType = 1
if (faceId >= 222){ if (faceId >= 222) {
faceType = 2 faceType = 2
} }
if (face?.AniStickerType){ if (face?.AniStickerType) {
faceType = 3; faceType = 3;
} }
return { return {

View File

@@ -1,29 +1,30 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTQQMsgApi, sendMessagePool } from './api/msg' import { NTQQMsgApi } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types' import {
CategoryFriend,
ChatType,
GroupMember,
GroupMemberRole,
RawMessage,
SimpleInfo, User,
} from './types'
import { import {
deleteGroup,
friends, friends,
getFriend, getFriend,
getGroupMember, getGroupMember,
groups, rawFriends, setSelfInfo
selfInfo,
tempGroupCodeMap,
uidMaps,
} 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, HOOK_LOG } from '@/common/config'
import fs from 'fs' import fs from 'node:fs'
import { dbUtil } from '@/common/db'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique'
import { isNumeric, sleep } from '@/common/utils' import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent' import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { randomUUID } from 'node:crypto'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -104,8 +105,8 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
if (hook.hookFunc.constructor.name === 'AsyncFunction') { if (hook.hookFunc.constructor.name === 'AsyncFunction') {
; (_ as Promise<void>).then() ; (_ as Promise<void>).then()
} }
} catch (e) { } catch (e: any) {
log('hook error', e, receiveData.payload) log('hook error', ntQQApiMethodName, e.stack.toString())
} }
}).then() }).then()
} }
@@ -234,28 +235,17 @@ 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) log('update group', group.groupCode)
if (group.privilegeFlag === 0) { if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode) deleteGroup(group.groupCode)
continue continue
} }
log('update group', group) //log('update group', group)
// if (!activatedGroups.includes(group.groupCode)) { NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().catch(log)
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group })
.then((r) => {
// activatedGroups.push(group.groupCode);
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
})
.catch(log)
// }
let existGroup = groups.find((g) => g.groupCode == group.groupCode) let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) { if (existGroup) {
Object.assign(existGroup, group) Object.assign(existGroup, group)
@@ -268,13 +258,13 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode) const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
if (members) { if (members) {
existGroup.members = members existGroup.members = Array.from(members.values())
} }
} }
} }
} }*/
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) {
@@ -287,20 +277,21 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时 await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode) const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = newMembers group.members = Array.from(newMembers.values())
const newMembersSet = new Set<string>() // 建立索引降低时间复杂度 const newMembersSet = new Set<string>() // 建立索引降低时间复杂度
for (const member of newMembers) { for (const member of newMembers) {
newMembersSet.add(member.uin) newMembersSet.add(member[1].uin)
} }
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢 // 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin) const selfUin = getSelfUin()
const bot = await getGroupMember(group.groupCode, selfUin)
if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) { if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue continue
} }
for (const member of oldMembers) { for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) { if (!newMembersSet.has(member.uin) && member.uin != selfUin) {
postOb11Event( postOb11Event(
new OB11GroupDecreaseEvent( new OB11GroupDecreaseEvent(
parseInt(group.groupCode), parseInt(group.groupCode),
@@ -324,12 +315,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) {
@@ -343,7 +334,7 @@ export async function startHook() {
}) })
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => { registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动, store", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
} }
@@ -352,7 +343,7 @@ export async function startHook() {
processGroupEvent(payload).then() processGroupEvent(payload).then()
} }
} }
}) })*/
registerReceiveHook<{ registerReceiveHook<{
groupCode: string groupCode: string
@@ -402,41 +393,37 @@ export async function startHook() {
registerReceiveHook<{ registerReceiveHook<{
data: CategoryFriend[] data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => { }>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0; // log("onBuddyListChange", payload)
rawFriends.push(...payload.data); // let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
for (const fData of payload.data) { type V2data = {userSimpleInfos: Map<string, SimpleInfo>}
const _friends = fData.buddyList let friendList: User[] = [];
for (let friend of _friends) { if ((payload as any).userSimpleInfos) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() // friendListV2 = payload as any
let existFriend = friends.find((f) => f.uin == friend.uin) friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
if (!existFriend) { return {
friends.push(friend) ...v.coreInfo,
}
else {
Object.assign(existFriend, friend)
} }
})
}
else{
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
}
}
log('好友列表变动', friendList)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
} }
} }
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid
const uin = message.senderUin
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin
}
}
// 自动清理新消息文件 // 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig() const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) { if (!autoDeleteFile) {
@@ -459,10 +446,6 @@ export async function startHook() {
if (msgElement.picElement) { if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath)) pathList.push(...Object.values(msgElement.picElement.thumbPath))
} }
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList); // log("需要清理的文件", pathList);
for (const path of pathList) { for (const path of pathList) {
@@ -478,23 +461,18 @@ export async function startHook() {
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord const { msgId, chatType, peerUid } = msgRecord
const peerUid = message.peerUid const peer = {
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); chatType,
// log("收到自己发送成功的消息", message.msgId, message.msgSeq); peerUid
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
} catch (e: any) {
log('receive self msg error', e.stack)
}
} }
MessageUnique.createMsg(peer, msgId)
}) })
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20 setSelfInfo({
online: info.info.status !== 20
})
}) })
let activatedPeerUids: string[] = [] let activatedPeerUids: string[] = []

View File

@@ -0,0 +1,240 @@
import { Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/ntqqapi/types'
interface IGroupListener {
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): void
onGroupExtListUpdate(...args: unknown[]): void
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]): void
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]): void
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): void
onGroupDetailInfoChange(...args: unknown[]): void
onGroupAllInfoChange(...args: unknown[]): void
onGroupsMsgMaskResult(...args: unknown[]): void
onGroupConfMemberChange(...args: unknown[]): void
onGroupBulletinChange(...args: unknown[]): void
onGetGroupBulletinListResult(...args: unknown[]): void
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>,
finish: boolean,
hasRobot: boolean
}): void
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>): void
onSearchMemberChange(...args: unknown[]): void
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]): void
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]): void
onGroupStatisticInfoChange(...args: unknown[]): void
onJoinGroupNotify(...args: unknown[]): void
onShutUpMemberListChanged(...args: unknown[]): void
onGroupBulletinRemindNotify(...args: unknown[]): void
onGroupFirstBulletinNotify(...args: unknown[]): void
onJoinGroupNoVerifyFlag(...args: unknown[]): void
onGroupArkInviteStateResult(...args: unknown[]): void
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void
}
export interface NodeIKernelGroupListener extends IGroupListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IGroupListener): NodeIKernelGroupListener
}
export class GroupListener implements IGroupListener {
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void {
}
onGetGroupBulletinListResult(...args: unknown[]) {
}
onGroupAllInfoChange(...args: unknown[]) {
}
onGroupBulletinChange(...args: unknown[]) {
}
onGroupBulletinRemindNotify(...args: unknown[]) {
}
onGroupArkInviteStateResult(...args: unknown[]) {
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
}
onGroupConfMemberChange(...args: unknown[]) {
}
onGroupDetailInfoChange(...args: unknown[]) {
}
onGroupExtListUpdate(...args: unknown[]) {
}
onGroupFirstBulletinNotify(...args: unknown[]) {
}
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
}
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
}
onGroupsMsgMaskResult(...args: unknown[]) {
}
onGroupStatisticInfoChange(...args: unknown[]) {
}
onJoinGroupNotify(...args: unknown[]) {
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
}
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean,
hasRobot: boolean
}) {
}
onSearchMemberChange(...args: unknown[]) {
}
onShutUpMemberListChanged(...args: unknown[]) {
}
}
export class DebugGroupListener implements IGroupListener {
onGroupMemberLevelInfoChange(...args: unknown[]): void {
console.log('onGroupMemberLevelInfoChange:', ...args)
}
onGetGroupBulletinListResult(...args: unknown[]) {
console.log('onGetGroupBulletinListResult:', ...args)
}
onGroupAllInfoChange(...args: unknown[]) {
console.log('onGroupAllInfoChange:', ...args)
}
onGroupBulletinChange(...args: unknown[]) {
console.log('onGroupBulletinChange:', ...args)
}
onGroupBulletinRemindNotify(...args: unknown[]) {
console.log('onGroupBulletinRemindNotify:', ...args)
}
onGroupArkInviteStateResult(...args: unknown[]) {
console.log('onGroupArkInviteStateResult:', ...args)
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
console.log('onGroupBulletinRichMediaDownloadComplete:', ...args)
}
onGroupConfMemberChange(...args: unknown[]) {
console.log('onGroupConfMemberChange:', ...args)
}
onGroupDetailInfoChange(...args: unknown[]) {
console.log('onGroupDetailInfoChange:', ...args)
}
onGroupExtListUpdate(...args: unknown[]) {
console.log('onGroupExtListUpdate:', ...args)
}
onGroupFirstBulletinNotify(...args: unknown[]) {
console.log('onGroupFirstBulletinNotify:', ...args)
}
onGroupListUpdate(...args: unknown[]) {
console.log('onGroupListUpdate:', ...args)
}
onGroupNotifiesUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUpdated:', ...args)
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
console.log('onGroupBulletinRichMediaProgressUpdate:', ...args)
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUnreadCountUpdated:', ...args)
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
console.log('onGroupSingleScreenNotifies:')
}
onGroupsMsgMaskResult(...args: unknown[]) {
console.log('onGroupsMsgMaskResult:', ...args)
}
onGroupStatisticInfoChange(...args: unknown[]) {
console.log('onGroupStatisticInfoChange:', ...args)
}
onJoinGroupNotify(...args: unknown[]) {
console.log('onJoinGroupNotify:', ...args)
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
console.log('onJoinGroupNoVerifyFlag:', ...args)
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
console.log('onMemberInfoChange:', groupCode, changeType, members)
}
onMemberListChange(...args: unknown[]) {
console.log('onMemberListChange:', ...args)
}
onSearchMemberChange(...args: unknown[]) {
console.log('onSearchMemberChange:', ...args)
}
onShutUpMemberListChanged(...args: unknown[]) {
console.log('onShutUpMemberListChanged:', ...args)
}
}

View File

@@ -0,0 +1,514 @@
import { ChatType, RawMessage } from '@/ntqqapi/types'
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string,
msgElementId: string,
msgId: string,
fileId: string,
fileProgress: string, // '0'
fileSpeed: string, // '0'
fileErrCode: string, // '0'
fileErrMsg: string,
fileDownType: number, // 暂时未知
thumbSize: number,
filePath: string,
totalSize: string,
trasferStatus: number,
step: number,
commonFileInfo: unknown | null,
fileSrvErrCode: string,
clientMsg: string,
businessId: number,
userTotalSpacePerDay: unknown | null,
userUsedSpacePerDay: unknown | null
}
export interface onGroupFileInfoUpdateParamType {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: Array<any>
allFileCount: string
nextIndex: string
reqId: string
}
// {
// sessionType: 1,
// chatType: 100,
// peerUid: 'u_PVQ3tl6K78xxxx',
// groupCode: '809079648',
// fromNick: '拾xxxx,
// sig: '0x'
// }
export interface TempOnRecvParams {
sessionType: number,//1
chatType: ChatType,//100
peerUid: string,//uid
groupCode: string,//gc
fromNick: string,//gc name
sig: string,
}
export interface IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage): void
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): void
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): void
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): void
onContactUnreadCntUpdate(hashMap: unknown): void
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): void
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): void
onEmojiDownloadComplete(emojiNotifyInfo: unknown): void
onEmojiResourceUpdate(emojiResourceInfo: unknown): void
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFileMsgCome(arrayList: unknown): void
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFirstViewGroupGuildMapping(arrayList: unknown): void
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): void
onGroupFileInfoAdd(groupItem: unknown): void
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType): void
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void
onGroupTransferInfoAdd(groupItem: unknown): void
onGroupTransferInfoUpdate(groupFileListResult: unknown): void
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): void
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): void
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): void
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): void
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): void
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): void
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): void
onInputStatusPush(inputStatusInfo: unknown): void
onKickedOffLine(kickedInfo: unknown): void
onLineDev(arrayList: unknown): void
onLogLevelChanged(j2: unknown): void
onMsgAbstractUpdate(arrayList: unknown): void
onMsgBoxChanged(arrayList: unknown): void
onMsgDelete(contact: unknown, arrayList: unknown): void
onMsgEventListUpdate(hashMap: unknown): void
onMsgInfoListAdd(arrayList: unknown): void
onMsgInfoListUpdate(msgList: RawMessage[]): void
onMsgQRCodeStatusChanged(i2: unknown): void
onMsgRecall(i2: unknown, str: unknown, j2: unknown): void
onMsgSecurityNotify(msgRecord: unknown): void
onMsgSettingUpdate(msgSetting: unknown): void
onNtFirstViewMsgSyncEnd(): void
onNtMsgSyncEnd(): void
onNtMsgSyncStart(): void
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onRecvGroupGuildFlag(i2: unknown): void
onRecvMsg(...arrayList: unknown[]): void
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): void
onRecvOnlineFileMsg(arrayList: unknown): void
onRecvS2CMsg(arrayList: unknown): void
onRecvSysMsg(arrayList: unknown): void
onRecvUDCFlag(i2: unknown): void
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): void
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): void
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): void
onSearchGroupFileInfoUpdate(searchGroupFileResult:
{
result: {
retCode: number,
retMsg: string,
clientWording: string
},
syncCookie: string,
totalMatchCount: number,
ownerMatchCount: number,
isEnd: boolean,
reqId: number,
item: Array<{
groupCode: string,
groupName: string,
uploaderUin: string,
uploaderName: string,
matchUin: string,
matchWords: Array<unknown>,
fileNameHits: Array<{
start: number,
end: number
}>,
fileModelId: string,
fileId: string,
fileName: string,
fileSize: string,
busId: number,
uploadTime: number,
modifyTime: number,
deadTime: number,
downloadTimes: number,
localPath: string
}>
}): void
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): void
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): void
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): void
onUnreadCntAfterFirstView(hashMap: unknown): void
onUnreadCntUpdate(hashMap: unknown): void
onUserChannelTabStatusChanged(z: unknown): void
onUserOnlineStatusChanged(z: unknown): void
onUserTabStatusChanged(arrayList: unknown): void
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]): void
onMsgWithRichLinkInfoUpdate(...args: unknown[]): void
onRedTouchChanged(...args: unknown[]): void
// 第一次发现于Win 9.9.9 23159
onBroadcastHelperProgerssUpdate(...args: unknown[]): void
}
export interface NodeIKernelMsgListener extends IKernelMsgListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IKernelMsgListener): NodeIKernelMsgListener
}
export class MsgListener implements IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage) {
}
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown) {
}
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown) {
}
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown) {
}
onContactUnreadCntUpdate(hashMap: unknown) {
}
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown) {
}
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown) {
}
onEmojiDownloadComplete(emojiNotifyInfo: unknown) {
}
onEmojiResourceUpdate(emojiResourceInfo: unknown) {
}
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFileMsgCome(arrayList: unknown) {
}
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFirstViewGroupGuildMapping(arrayList: unknown) {
}
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown) {
}
onGroupFileInfoAdd(groupItem: unknown) {
}
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType) {
}
onGroupGuildUpdate(groupGuildNotifyInfo: unknown) {
}
onGroupTransferInfoAdd(groupItem: unknown) {
}
onGroupTransferInfoUpdate(groupFileListResult: unknown) {
}
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown) {
}
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown) {
}
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown) {
}
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown) {
}
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown) {
}
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown) {
}
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown) {
}
onInputStatusPush(inputStatusInfo: unknown) {
}
onKickedOffLine(kickedInfo: unknown) {
}
onLineDev(arrayList: unknown) {
}
onLogLevelChanged(j2: unknown) {
}
onMsgAbstractUpdate(arrayList: unknown) {
}
onMsgBoxChanged(arrayList: unknown) {
}
onMsgDelete(contact: unknown, arrayList: unknown) {
}
onMsgEventListUpdate(hashMap: unknown) {
}
onMsgInfoListAdd(arrayList: unknown) {
}
onMsgInfoListUpdate(msgList: RawMessage[]) {
}
onMsgQRCodeStatusChanged(i2: unknown) {
}
onMsgRecall(i2: unknown, str: unknown, j2: unknown) {
}
onMsgSecurityNotify(msgRecord: unknown) {
}
onMsgSettingUpdate(msgSetting: unknown) {
}
onNtFirstViewMsgSyncEnd() {
}
onNtMsgSyncEnd() {
}
onNtMsgSyncStart() {
}
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onRecvGroupGuildFlag(i2: unknown) {
}
onRecvMsg(arrayList: RawMessage[]) {
}
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown) {
}
onRecvOnlineFileMsg(arrayList: unknown) {
}
onRecvS2CMsg(arrayList: unknown) {
}
onRecvSysMsg(arrayList: unknown) {
}
onRecvUDCFlag(i2: unknown) {
}
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) {
}
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown) {
}
onRichMediaUploadComplete(fileTransNotifyInfo: unknown) {
}
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown) {
}
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown) {
}
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown) {
}
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams) {
}
onUnreadCntAfterFirstView(hashMap: unknown) {
}
onUnreadCntUpdate(hashMap: unknown) {
}
onUserChannelTabStatusChanged(z: unknown) {
}
onUserOnlineStatusChanged(z: unknown) {
}
onUserTabStatusChanged(arrayList: unknown) {
}
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]) {
}
onMsgWithRichLinkInfoUpdate(...args: unknown[]) {
}
onRedTouchChanged(...args: unknown[]) {
}
// 第一次发现于Win 9.9.9-23159
onBroadcastHelperProgerssUpdate(...args: unknown[]) {
}
}

View File

@@ -1 +1,3 @@
export * from './NodeIKernelProfileListener' export * from './NodeIKernelProfileListener'
export * from './NodeIKernelGroupListener'
export * from './NodeIKernelMsgListener'

View File

@@ -1,19 +0,0 @@
import * as os from "os";
import path from "node:path";
import fs from "fs";
export function getModuleWithArchName(moduleName: string) {
const systemPlatform = os.platform()
const cpuArch = os.arch()
return `${moduleName}-${systemPlatform}-${cpuArch}.node`
}
export function cpModule(moduleName: string) {
const currentDir = path.resolve(__dirname);
const fileName = `./${getModuleWithArchName(moduleName)}`
try {
fs.copyFileSync(path.join(currentDir, fileName), path.join(currentDir, `${moduleName}.node`));
} catch (e) {
}
}

View File

@@ -1,58 +0,0 @@
import { log } from '../../../common/utils'
import { NTQQApi } from '../../ntcall'
import { cpModule } from '../cpmodule'
type PokeHandler = (id: string, isGroup: boolean) => void
type CrychicHandler = (event: string, id: string, isGroup: boolean) => void
let pokeRecords: Record<string, number> = {}
class Crychic {
private crychic: any = undefined
loadNode() {
if (!this.crychic) {
try {
cpModule('crychic')
this.crychic = require('./crychic.node')
this.crychic.init()
} catch (e) {
log('crychic加载失败', e)
}
}
}
registerPokeHandler(fn: PokeHandler) {
this.registerHandler((event, id, isGroup) => {
if (event === 'poke') {
let existTime = pokeRecords[id]
if (existTime) {
if (Date.now() - existTime < 1500) {
return
}
}
pokeRecords[id] = Date.now()
fn(id, isGroup)
}
})
}
registerHandler(fn: CrychicHandler) {
if (!this.crychic) return
this.crychic.setCryHandler(fn)
}
sendFriendPoke(friendUid: string) {
if (!this.crychic) return
this.crychic.sendFriendPoke(parseInt(friendUid))
NTQQApi.fetchUnitedCommendConfig().then()
}
sendGroupPoke(groupCode: string, memberUin: string) {
if (!this.crychic) return
this.crychic.sendGroupPoke(parseInt(memberUin), parseInt(groupCode))
NTQQApi.fetchUnitedCommendConfig().then()
}
}
export const crychic = new Crychic()

View File

@@ -1,33 +0,0 @@
import {cpModule} from "../cpmodule";
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface MoeHook {
GetRkey: () => string, // Return '&rkey=xxx'
HookRkey: (version: string) => string
}
class HookApi {
private readonly moeHook: MoeHook | null = null;
constructor() {
cpModule('MoeHoo');
try {
this.moeHook = require('./MoeHoo.node');
console.log("hook rkey qq version", this.moeHook!.HookRkey(qqPkgInfo.version));
console.log("hook rkey地址", this.moeHook!.HookRkey(qqPkgInfo.version));
} catch (e) {
console.log('加载 moehoo 失败', e);
}
}
getRKey(): string {
return this.moeHook?.GetRkey() || '';
}
isAvailable() {
return !!this.moeHook;
}
}
// export const hookApi = new HookApi();

View File

@@ -0,0 +1,249 @@
import { NodeIKernelGroupListener } from '@/ntqqapi/listeners'
import {
GroupExtParam,
GroupMember,
GroupMemberRole,
GroupNotifyTypes,
GroupRequestOperateTypes,
} from '@/ntqqapi/types'
import { GeneralCallResult } from './common'
//高版本的接口不应该随意使用 使用应该严格进行pr审核 同时部分ipc中未出现的接口不要过于依赖 应该做好数据兜底
export interface NodeIKernelGroupService {
getMemberCommonInfo(Req: {
groupCode: string,
startUin: string,
identifyFlag: string,
uinList: string[],
memberCommonFilter: {
memberUin: number,
uinFlag: number,
uinFlagExt: number,
uinMobileFlag: number,
shutUpTime: number,
privilege: number,
},
memberNum: number,
filterMethod: string,
onlineFlag: string,
realSpecialTitleFlag: number
}): Promise<unknown>
//26702
getGroupMemberLevelInfo(groupCode: string): Promise<unknown>
//26702
getGroupHonorList(groupCodes: Array<string>): unknown
getUinByUids(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uins: Map<string, string>
}>
getUidByUins(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uids: Map<string, string>
}>
//26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
//26702(其实更早 但是我不知道)
getGroupLatestEssenceList(groupCode: string): Promise<unknown>
//26702(其实更早 但是我不知道)
shareDigest(Req: {
appId: string,
appType: number,
msgStyle: number,
recvUin: string,
sendType: number,
clientInfo: {
platform: number
},
richMsg: {
usingArk: boolean,
title: string,
summary: string,
url: string,
pictureUrl: string,
brief: string
}
}): Promise<unknown>
//26702(其实更早 但是我不知道)
isEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
queryCachedEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
fetchGroupEssenceList(Req: { groupCode: string, pageStart: number, pageLimit: number }, Arg: unknown): Promise<unknown>
//26702
getAllMemberList(groupCode: string, forceFetch: boolean): Promise<{
errCode: number,
errMsg: string,
result: {
ids: Array<{
uid: string,
index: number//0
}>,
infos: {},
finish: true,
hasRobot: false
}
}>
setHeader(uid: string, path: string): unknown
addKernelGroupListener(listener: NodeIKernelGroupListener): number
removeKernelGroupListener(listenerId: unknown): void
createMemberListScene(groupCode: string, scene: string): string
destroyMemberListScene(SceneId: string): void
//About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string}
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number, errMsg: string,
result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean }
}>
getPrevMemberList(): unknown
monitorMemberList(): unknown
searchMember(sceneId: string, keywords: string[]): unknown
getMemberInfo(group_id: string, uids: string[], forceFetch: boolean): Promise<GeneralCallResult>
//getMemberInfo [ '56729xxxx', [ 'u_4Nj08cwW5Hxxxxx' ], true ]
kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>
modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void
modifyMemberCardName(groupCode: string, uid: string, cardName: string): void
getTransferableMemberInfo(groupCode: string): unknown//获取整个群的
transferGroup(uid: string): void
getGroupList(force: boolean): Promise<GeneralCallResult>
getGroupExtList(force: boolean): Promise<GeneralCallResult>
getGroupDetailInfo(groupCode: string): unknown
getMemberExtInfo(param: GroupExtParam): Promise<unknown>//req
getGroupAllInfo(): unknown
getDiscussExistInfo(): unknown
getGroupConfMember(): unknown
getGroupMsgMask(): unknown
getGroupPortrait(): void
modifyGroupName(groupCode: string, groupName: string, arg: false): void
modifyGroupRemark(groupCode: string, remark: string): void
modifyGroupDetailInfo(groupCode: string, arg: unknown): void
setGroupMsgMask(groupCode: string, arg: unknown): void
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void
inviteToGroup(arg: unknown): void
inviteMembersToGroup(args: unknown[]): void
inviteMembersToGroupWithMsg(args: unknown): void
createGroup(arg: unknown): void
createGroupWithMembers(arg: unknown): void
quitGroup(groupCode: string): void
destroyGroup(groupCode: string): void
//获取单屏群通知列表
getSingleScreenNotifies(force: boolean, start_seq: string, num: number): Promise<GeneralCallResult>
clearGroupNotifies(groupCode: string): void
getGroupNotifiesUnreadCount(unknown: Boolean): Promise<GeneralCallResult>
clearGroupNotifiesUnreadCount(groupCode: string): void
operateSysNotify(
doubt: boolean,
operateMsg: {
operateType: GroupRequestOperateTypes, // 2 拒绝
targetMsg: {
seq: string, // 通知序列号
type: GroupNotifyTypes,
groupCode: string,
postscript: string
}
}): Promise<void>
setTop(groupCode: string, isTop: boolean): void
getGroupBulletin(groupCode: string): unknown
deleteGroupBulletin(groupCode: string, seq: string): void
publishGroupBulletin(groupCode: string, pskey: string, data: any): Promise<GeneralCallResult>
publishInstructionForNewcomers(groupCode: string, arg: unknown): void
uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<GeneralCallResult & {
errCode: number
picInfo?: {
id: string,
width: number,
height: number
}
}>
downloadGroupBulletinRichMedia(groupCode: string): unknown
getGroupBulletinList(groupCode: string): unknown
getGroupStatisticInfo(groupCode: string): unknown
getGroupRemainAtTimes(groupCode: string): number
getJoinGroupNoVerifyFlag(groupCode: string): unknown
getGroupArkInviteState(groupCode: string): unknown
reqToJoinGroup(groupCode: string, arg: unknown): void
setGroupShutUp(groupCode: string, shutUp: boolean): void
getGroupShutUpMemberList(groupCode: string): unknown[]
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>
getGroupRecommendContactArkJson(groupCode: string): unknown
getJoinGroupLink(groupCode: string): unknown
modifyGroupExtInfo(groupCode: string, arg: unknown): void
//需要提前判断是否存在 高版本新增
addGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>
//需要提前判断是否存在 高版本新增
removeGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,3 @@
export interface NodeIKernelMSFService {
getServerTime(): string
}

View File

@@ -0,0 +1,744 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { NodeIKernelMsgListener } from '@/ntqqapi/listeners/NodeIKernelMsgListener'
import { GeneralCallResult } from './common'
export interface QueryMsgsParams {
chatInfo: Peer,
filterMsgType: [],
filterSendersUid: string[],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi {
errMsg: string
result: number
tmpChatInfo?: TmpChatInfo
}
export interface TmpChatInfo {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string
addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>): Promise<GeneralCallResult>
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>
addKernelMsgImportToolListener(arg: Object): unknown
removeKernelMsgListener(args: unknown): unknown
addKernelTempChatSigListener(...args: unknown[]): unknown
removeKernelTempChatSigListener(...args: unknown[]): unknown
setAutoReplyTextList(AutoReplyText: Array<unknown>, i2: number): unknown
getAutoReplyTextList(...args: unknown[]): unknown
getOnLineDev(): void
kickOffLine(DevInfo: Object): unknown
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
fetchStatusMgrInfo(): unknown
fetchStatusUnitedConfigInfo(): unknown
getOnlineStatusSmallIconBasePath(): unknown
getOnlineStatusSmallIconFileNameByUrl(Url: string): unknown
downloadOnlineStatusSmallIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusBigIconBasePath(): unknown
downloadOnlineStatusBigIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusCommonPath(arg: string): unknown
getOnlineStatusCommonFileNameByUrl(Url: string): unknown
downloadOnlineStatusCommonByUrl(arg0: string, arg1: string): unknown
// this.tokenType = i2
// this.apnsToken = bArr
// this.voipToken = bArr2
// this.profileId = str
setToken(arg: Object): unknown
switchForeGround(): unknown
switchBackGround(arg: Object): unknown
//hex
setTokenForMqq(token: string): unknown
switchForeGroundForMqq(...args: unknown[]): unknown
switchBackGroundForMqq(...args: unknown[]): unknown
getMsgSetting(...args: unknown[]): unknown
setMsgSetting(...args: unknown[]): unknown
addSendMsg(...args: unknown[]): unknown
cancelSendMsg(...args: unknown[]): unknown
switchToOfflineSendMsg(peer: Peer, MsgId: string): unknown
reqToOfflineSendMsg(...args: unknown[]): unknown
refuseReceiveOnlineFileMsg(peer: Peer, MsgId: string): unknown
resendMsg(...args: unknown[]): unknown
recallMsg(...args: unknown[]): unknown
reeditRecallMsg(...args: unknown[]): unknown
//调用请检查除开commentElements其余参数不能为null
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
forwardMsgWithComment(...args: unknown[]): unknown
forwardSubMsgWithComment(...args: unknown[]): unknown
forwardRichMsgInVist(...args: unknown[]): unknown
forwardFile(...args: unknown[]): unknown
//Array<Msg>, Peer from, Peer to
multiForwardMsg(...args: unknown[]): unknown
multiForwardMsgWithComment(...args: unknown[]): unknown
deleteRecallMsg(...args: unknown[]): unknown
deleteRecallMsgForLocal(...args: unknown[]): unknown
addLocalGrayTipMsg(...args: unknown[]): unknown
addLocalJsonGrayTipMsg(...args: unknown[]): unknown
addLocalJsonGrayTipMsgExt(...args: unknown[]): unknown
IsLocalJsonTipValid(...args: unknown[]): unknown
addLocalAVRecordMsg(...args: unknown[]): unknown
addLocalTofuRecordMsg(...args: unknown[]): unknown
addLocalRecordMsg(Peer: Peer, msgId: string, ele: MessageElement, attr: Array<any> | number, front: boolean): Promise<unknown>
deleteMsg(Peer: Peer, msgIds: Array<string>): Promise<any>
updateElementExtBufForUI(...args: unknown[]): unknown
updateMsgRecordExtPbBufForUI(...args: unknown[]): unknown
startMsgSync(...args: unknown[]): unknown
startGuildMsgSync(...args: unknown[]): unknown
isGuildChannelSync(...args: unknown[]): unknown
getMsgUniqueId(UniqueId: string): string
isMsgMatched(...args: unknown[]): unknown
getOnlineFileMsgs(...args: unknown[]): unknown
getAllOnlineFileMsgs(...args: unknown[]): unknown
getLatestDbMsgs(peer: Peer, cnt: number): Promise<unknown>
getLastMessageList(peer: Peer[]): Promise<unknown>
getAioFirstViewLatestMsgs(peer: Peer, num: number): unknown
//deprecated 从9.9.15-26702版本开始该接口已经废弃请使用getMsgsEx
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
// this.$peer = contact
// this.$msgTime = j2
// this.$clientSeq = j3
// this.$cnt = i2
getMsgsWithMsgTimeAndClientSeqForC2C(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsWithStatus(params: {
peer: Peer
msgId: string
msgTime: unknown
cnt: unknown
queryOrder: boolean
isIncludeSelf: boolean
appid: unknown
}): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqRange(peer: Peer, startSeq: string, endSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsByMsgId(peer: Peer, ids: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getRecallMsgsByMsgId(peer: Peer, MsgId: string[]): Promise<unknown>
getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSourceOfReplyMsg(peer: Peer, MsgId: string, SourceSeq: string): unknown
getSourceOfReplyMsgV2(peer: Peer, RootMsgId: string, ReplyMsgId: string): unknown
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
//cnt clientSeq?并不是吧
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown
queryMsgsWithFilter(...args: unknown[]): unknown
/**
* @deprecated 该函数已被标记为废弃,请使用新的替代方法。
* 使用过滤条件查询消息列表的版本2接口。
*
* 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
* 函数返回一个Promise解析为查询结果的未知类型对象。
*
* @param MsgId 消息ID用于特定消息的查询。
* @param MsgTime 消息时间,用于指定消息的时间范围。
* @param param 查询参数对象,包含详细的过滤条件和分页信息。
* @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
* @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
* @param param.filterSendersUid 需要过滤的发送者用户ID数组。
* @param param.filterMsgFromTime 查询消息的起始时间。
* @param param.filterMsgToTime 查询消息的结束时间。
* @param param.pageLimit 每页的消息数量限制。
* @param param.isReverseOrder 是否按时间顺序倒序返回消息。
* @param param.isIncludeCurrent 是否包含当前页码。
* @returns 返回一个Promise解析为查询结果的未知类型对象。
*/
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>
// this.chatType = i2
// this.peerUid = str
// this.chatInfo = new ChatInfo()
// this.filterMsgType = new ArrayList<>()
// this.filterSendersUid = new ArrayList<>()
// this.chatInfo = chatInfo
// this.filterMsgType = arrayList
// this.filterSendersUid = arrayList2
// this.filterMsgFromTime = j2
// this.filterMsgToTime = j3
// this.pageLimit = i2
// this.isReverseOrder = z
// this.isIncludeCurrent = z2
//queryMsgsWithFilterEx(0L, 0L, 0L, new QueryMsgsParams(new ChatInfo(2, str), new ArrayList(), new ArrayList(), 0L, 0L, 250, false, true))
queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
//queryMsgsWithFilterEx(this.$msgId, this.$msgTime, this.$msgSeq, this.$param)
queryFileMsgsDesktop(...args: unknown[]): unknown
setMsgRichInfoFlag(...args: unknown[]): unknown
queryPicOrVideoMsgs(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<unknown>
queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown
queryEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryTroopEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryMsgsAndAbstractsWithFilter(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): unknown
setFocusOnGuild(...args: unknown[]): unknown
setFocusSession(...args: unknown[]): unknown
enableFilterUnreadInfoNotify(...args: unknown[]): unknown
enableFilterMsgAbstractNotify(...args: unknown[]): unknown
onScenesChangeForSilenceMode(...args: unknown[]): unknown
getContactUnreadCnt(...args: unknown[]): unknown
getUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntTabInfo(...args: unknown[]): unknown
getAllGuildUnreadCntInfo(...args: unknown[]): unknown
getAllJoinGuildCnt(...args: unknown[]): unknown
getAllDirectSessionUnreadCntInfo(...args: unknown[]): unknown
getCategoryUnreadCntInfo(...args: unknown[]): unknown
getGuildFeedsUnreadCntInfo(...args: unknown[]): unknown
setUnVisibleChannelCntInfo(...args: unknown[]): unknown
setUnVisibleChannelTypeCntInfo(...args: unknown[]): unknown
setVisibleGuildCntInfo(...args: unknown[]): unknown
setMsgRead(peer: Peer): Promise<GeneralCallResult>
setAllC2CAndGroupMsgRead(): Promise<unknown>
setGuildMsgRead(...args: unknown[]): unknown
setAllGuildMsgRead(...args: unknown[]): unknown
setMsgReadAndReport(...args: unknown[]): unknown
setSpecificMsgReadAndReport(...args: unknown[]): unknown
setLocalMsgRead(...args: unknown[]): unknown
setGroupGuildMsgRead(...args: unknown[]): unknown
getGuildGroupTransData(...args: unknown[]): unknown
setGroupGuildBubbleRead(...args: unknown[]): unknown
getGuildGroupBubble(...args: unknown[]): unknown
fetchGroupGuildUnread(...args: unknown[]): unknown
setGroupGuildFlag(...args: unknown[]): unknown
setGuildUDCFlag(...args: unknown[]): unknown
setGuildTabUserFlag(...args: unknown[]): unknown
setBuildMode(flag: number/*0 1 3*/): unknown
setConfigurationServiceData(...args: unknown[]): unknown
setMarkUnreadFlag(...args: unknown[]): unknown
getChannelEventFlow(...args: unknown[]): unknown
getMsgEventFlow(...args: unknown[]): unknown
getRichMediaFilePathForMobileQQSend(...args: unknown[]): unknown
getRichMediaFilePathForGuild(arg: {
md5HexStr: string,
fileName: string,
elementType: ElementType,
elementSubType: number,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
}): string
assembleMobileQQRichMediaFilePath(...args: unknown[]): unknown
getFileThumbSavePathForSend(...args: unknown[]): unknown
getFileThumbSavePath(...args: unknown[]): unknown
//猜测居多
translatePtt2Text(MsgId: string, Peer: {}, MsgElement: {}): unknown
setPttPlayedState(...args: unknown[]): unknown
// NodeIQQNTWrapperSession fetchFavEmojiList [
// "",
// 48,
// true,
// true
// ]
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: Array<{
uin: string,
emoId: number,
emoPath: string,
isExist: boolean,
resId: string,
url: string,
md5: string,
emoOriginalPath: string,
thumbPath: string,
RomaingType: string,
isAPNG: false,
isMarkFace: false,
eId: string,
epId: string,
ocrWord: string,
modifyWord: string,
exposeNum: number,
clickNum: number,
desc: string
}>
}>
addFavEmoji(...args: unknown[]): unknown
fetchMarketEmoticonList(...args: unknown[]): unknown
fetchMarketEmoticonShowImage(...args: unknown[]): unknown
fetchMarketEmoticonAioImage(...args: unknown[]): unknown
fetchMarketEmotionJsonFile(...args: unknown[]): unknown
getMarketEmoticonPath(...args: unknown[]): unknown
getMarketEmoticonPathBySync(...args: unknown[]): unknown
fetchMarketEmoticonFaceImages(...args: unknown[]): unknown
fetchMarketEmoticonAuthDetail(...args: unknown[]): unknown
getFavMarketEmoticonInfo(...args: unknown[]): unknown
addRecentUsedFace(...args: unknown[]): unknown
getRecentUsedFaceList(...args: unknown[]): unknown
getMarketEmoticonEncryptKeys(...args: unknown[]): unknown
downloadEmojiPic(...args: unknown[]): unknown
deleteFavEmoji(...args: unknown[]): unknown
modifyFavEmojiDesc(...args: unknown[]): unknown
queryFavEmojiByDesc(...args: unknown[]): unknown
getHotPicInfoListSearchString(...args: unknown[]): unknown
getHotPicSearchResult(...args: unknown[]): unknown
getHotPicHotWords(...args: unknown[]): unknown
getHotPicJumpInfo(...args: unknown[]): unknown
getEmojiResourcePath(...args: unknown[]): unknown
JoinDragonGroupEmoji(JoinDragonGroupEmojiReq: any/*joinDragonGroupEmojiReq*/): unknown
getMsgAbstracts(...args: unknown[]): unknown
getMsgAbstract(...args: unknown[]): unknown
getMsgAbstractList(...args: unknown[]): unknown
getMsgAbstractListBySeqRange(...args: unknown[]): unknown
refreshMsgAbstracts(...args: unknown[]): unknown
refreshMsgAbstractsByGuildIds(...args: unknown[]): unknown
getRichMediaElement(...args: unknown[]): unknown
cancelGetRichMediaElement(...args: unknown[]): unknown
refuseGetRichMediaElement(...args: unknown[]): unknown
switchToOfflineGetRichMediaElement(...args: unknown[]): unknown
downloadRichMedia(...args: unknown[]): unknown
getFirstUnreadMsgSeq(args: {
peerUid: string
guildId: string
}): unknown
getFirstUnreadCommonMsg(...args: unknown[]): unknown
getFirstUnreadAtmeMsg(...args: unknown[]): unknown
getFirstUnreadAtallMsg(...args: unknown[]): unknown
getNavigateInfo(...args: unknown[]): unknown
getChannelFreqLimitInfo(...args: unknown[]): unknown
getRecentUseEmojiList(...args: unknown[]): unknown
getRecentEmojiList(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number,
errMsg: string,
emojiLikesList:
Array<{
tinyId: string,
nickName: string,
headUrl: string
}>,
cookie: string,
isLastPage: boolean,
isFirstPage: boolean
}>
setMsgEmojiLikesForRole(...args: unknown[]): unknown
clickInlineKeyboardButton(...args: unknown[]): unknown
setCurOnScreenMsg(...args: unknown[]): unknown
setCurOnScreenMsgForMsgEvent(...args: unknown[]): unknown
getMiscData(key: string): unknown
setMiscData(key: string, value: string): unknown
getBookmarkData(...args: unknown[]): unknown
setBookmarkData(...args: unknown[]): unknown
sendShowInputStatusReq(ChatType: number, EventType: number, toUid: string): Promise<unknown>
queryCalendar(...args: unknown[]): unknown
queryFirstMsgSeq(peer: Peer, ...args: unknown[]): unknown
queryRoamCalendar(...args: unknown[]): unknown
queryFirstRoamMsg(...args: unknown[]): unknown
fetchLongMsg(peer: Peer, msgId: string): unknown
fetchLongMsgWithCb(...args: unknown[]): unknown
setIsStopKernelFetchLongMsg(...args: unknown[]): unknown
insertGameResultAsMsgToDb(...args: unknown[]): unknown
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
setDraft(...args: unknown[]): unknown
getDraft(...args: unknown[]): unknown
deleteDraft(...args: unknown[]): unknown
getRecentHiddenSesionList(...args: unknown[]): unknown
setRecentHiddenSession(...args: unknown[]): unknown
delRecentHiddenSession(...args: unknown[]): unknown
getCurHiddenSession(...args: unknown[]): unknown
setCurHiddenSession(...args: unknown[]): unknown
setReplyDraft(...args: unknown[]): unknown
getReplyDraft(...args: unknown[]): unknown
deleteReplyDraft(...args: unknown[]): unknown
getFirstUnreadAtMsg(peer: Peer): unknown
clearMsgRecords(...args: unknown[]): unknown//设置已读后调用我觉得比较好 清理记录 现在别了
IsExistOldDb(...args: unknown[]): unknown
canImportOldDbMsg(...args: unknown[]): unknown
setPowerStatus(z: boolean): unknown
canProcessDataMigration(...args: unknown[]): unknown
importOldDbMsg(...args: unknown[]): unknown
stopImportOldDbMsgAndroid(...args: unknown[]): unknown
isMqqDataImportFinished(...args: unknown[]): unknown
getMqqDataImportTableNames(...args: unknown[]): unknown
getCurChatImportStatusByUin(...args: unknown[]): unknown
getDataImportUserLevel(): unknown
getMsgQRCode(...args: unknown[]): unknown
getGuestMsgAbstracts(...args: unknown[]): unknown
getGuestMsgByRange(...args: unknown[]): unknown
getGuestMsgAbstractByRange(...args: unknown[]): unknown
registerSysMsgNotification(...args: unknown[]): unknown
unregisterSysMsgNotification(...args: unknown[]): unknown
enterOrExitAio(...args: unknown[]): unknown
// this.peerUid = ""
// this.peerNickname = ""
// this.fromGroupCode = ""
// this.sig = new byte[0]
// this.selfUid = ""
// this.selfPhone = ""
// this.chatType = i2
// this.peerUid = str
// this.peerNickname = str2
// this.fromGroupCode = str3
// this.sig = bArr
// this.selfUid = str4
// this.selfPhone = str5
// this.gameSession = tempChatGameSession
prepareTempChat(args: unknown): unknown//主动临时消息 不做
sendSsoCmdReqByContend(cmd: string, param: string): Promise<unknown>
//chattype,uid->Promise<any>
getTempChatInfo(ChatType: number, Uid: string): Promise<TmpChatInfoApi>
setContactLocalTop(...args: unknown[]): unknown
switchAnonymousChat(...args: unknown[]): unknown
renameAnonyChatNick(...args: unknown[]): unknown
getAnonymousInfo(...args: unknown[]): unknown
updateAnonymousInfo(...args: unknown[]): unknown
sendSummonMsg(peer: Peer, MsgElement: unknown, MsgAttributeInfo: unknown): Promise<unknown>//频道的东西
outputGuildUnreadInfo(...args: unknown[]): unknown
checkMsgWithUrl(...args: unknown[]): unknown
checkTabListStatus(...args: unknown[]): unknown
getABatchOfContactMsgBoxInfo(...args: unknown[]): unknown
insertMsgToMsgBox(peer: Peer, msgId: string, arg: 2006): unknown
isHitEmojiKeyword(...args: unknown[]): unknown
getKeyWordRelatedEmoji(...args: unknown[]): unknown
recordEmoji(...args: unknown[]): unknown
fetchGetHitEmotionsByWord(args: Object): Promise<unknown>//表情推荐?
deleteAllRoamMsgs(...args: unknown[]): unknown//漫游消息?
packRedBag(...args: unknown[]): unknown
grabRedBag(...args: unknown[]): unknown
pullDetail(...args: unknown[]): unknown
selectPasswordRedBag(...args: unknown[]): unknown
pullRedBagPasswordList(...args: unknown[]): unknown
requestTianshuAdv(...args: unknown[]): unknown
tianshuReport(...args: unknown[]): unknown
tianshuMultiReport(...args: unknown[]): unknown
GetMsgSubType(a0: number, a1: number): unknown
setIKernelPublicAccountAdapter(...args: unknown[]): unknown
//tempChatGameSession有关
createUidFromTinyId(fromTinyId: string, toTinyId: string): unknown
dataMigrationGetDataAvaiableContactList(...args: unknown[]): unknown
dataMigrationGetMsgList(...args: unknown[]): unknown
dataMigrationStopOperation(...args: unknown[]): unknown
//新的希望
dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{
extensionData: string//"Hex"
extraData: string //""
chatType: number
chatUin: string
msgType: number
msgTime: string
msgSeq: string
msgRandom: string
}>, DataMigrationResourceInfo: {
extraData: string
filePath: string
fileSize: string
msgRandom: string
msgSeq: string
msgSubType: number
msgType: number
}): unknown
dataMigrationGetResourceLocalDestinyPath(...args: unknown[]): unknown
dataMigrationSetIOSPathPrefix(...args: unknown[]): unknown
getServiceAssistantSwitch(...args: unknown[]): unknown
setServiceAssistantSwitch(...args: unknown[]): unknown
setSubscribeFolderUsingSmallRedPoint(...args: unknown[]): unknown
clearGuildNoticeRedPoint(...args: unknown[]): unknown
clearFeedNoticeRedPoint(...args: unknown[]): unknown
clearFeedSquareRead(...args: unknown[]): unknown
IsC2CStyleChatType(...args: unknown[]): unknown
IsTempChatType(uin: number): unknown//猜的
getGuildInteractiveNotification(...args: unknown[]): unknown
getGuildNotificationAbstract(...args: unknown[]): unknown
setFocusOnBase(...args: unknown[]): unknown
queryArkInfo(...args: unknown[]): unknown
queryUserSecQuality(...args: unknown[]): unknown
getGuildMsgAbFlag(...args: unknown[]): unknown
getGroupMsgStorageTime(): unknown//这是嘛啊
}

View File

@@ -0,0 +1,22 @@
import { BuddyProfileLikeReq } from '../types'
import { GeneralCallResult } from './common'
export interface NodeIKernelProfileLikeService {
addKernelProfileLikeListener(listener: NodeIKernelProfileLikeService): void
removeKernelProfileLikeListener(listener: unknown): void
setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number }
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
'info': {
'userLikeInfos': Array<any>,
'friendMaxVotes': number,
'start': number
}
}>
getProfileLikeScidResourceInfo(...args: unknown[]): void
isNull(): boolean
}

View File

@@ -0,0 +1,270 @@
import { GetFileListParam, MessageElement, Peer } from '../types'
import { GeneralCallResult } from './common'
export enum UrlFileDownloadType {
KUNKNOWN,
KURLFILEDOWNLOADPRIVILEGEICON,
KURLFILEDOWNLOADPHOTOWALL,
KURLFILEDOWNLOADQZONE,
KURLFILEDOWNLOADCOMMON,
KURLFILEDOWNLOADINSTALLAPP
}
export enum RMBizTypeEnum {
KUNKNOWN,
KC2CFILE,
KGROUPFILE,
KC2CPIC,
KGROUPPIC,
KDISCPIC,
KC2CVIDEO,
KGROUPVIDEO,
KC2CPTT,
KGROUPPTT,
KFEEDCOMMENTPIC,
KGUILDFILE,
KGUILDPIC,
KGUILDPTT,
KGUILDVIDEO
}
export interface CommonFileInfo {
bizType: number
chatType: number
elemId: string
favId: string
fileModelId: string
fileName: string
fileSize: string
md5: string
md510m: string
msgId: string
msgTime: string
parent: string
peerUid: string
picThumbPath: Array<string>
sha: string
sha3: string
subId: string
uuid: string
}
export interface NodeIKernelRichMediaService {
//getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb)
// public enum VideoCodecFormatType {
// KCODECFORMATH264,
// KCODECFORMATH265,
// KCODECFORMATH266,
// KCODECFORMATAV1
// }
// public enum VideoRequestWay {
// KUNKNOW,
// KHAND,
// KAUTO
// }
getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise<unknown>
//exParams (RMReqExParams)
// this.downSourceType = i2
// this.triggerType = i3
//peer, msgId, elemId, videoCodecFormat, exParams
// 1 0 频道在用
// 1 1
// 0 2
// public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007
// public static final int KDOWNSOURCETYPEAIOINNER = 1
// public static final int KDOWNSOURCETYPEBIGSCREEN = 2
// public static final int KDOWNSOURCETYPEHISTORY = 3
// public static final int KDOWNSOURCETYPEUNKNOWN = 0
// public static final int KTRIGGERTYPEAUTO = 1
// public static final int KTRIGGERTYPEMANUAL = 0
getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & {
urlResult: {
v4IpUrl: [],
v6IpUrl: [],
domainUrl: Array<{
url: string,
isHttps: boolean,
httpsDomain: string
}>,
videoCodecFormat: number
}
}>
getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown
// this.senderUid = ""
// this.peerUid = ""
// this.guildId = ""
// this.elem = new MsgElement()
// this.downloadType = i2
// this.thumbSize = i3
// this.msgId = j2
// this.msgRandom = j3
// this.msgSeq = j4
// this.msgTime = j5
// this.chatType = i4
// this.senderUid = str
// this.peerUid = str2
// this.guildId = str3
// this.elem = msgElement
// this.useHttps = num
getVideoPlayUrlInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): Promise<unknown>
//arg双端number
isFileExpired(arg: number): unknown
deleteGroupFolder(GroupCode: string, FolderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>
//参数与getVideoPlayUrlInVisit一样
downloadRichMediaInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): unknown
//arg3为“”
downloadFileForModelId(peer: Peer, ModelId: string[], arg3: string): unknown
//第三个参数 Array<Type>
// this.fileId = ""
// this.fileName = ""
// this.fileId = str
// this.fileName = str2
// this.fileSize = j2
// this.fileModelId = j3
downloadFileForFileUuid(peer: Peer, uuid: string, arg3: {
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}[]): Promise<unknown>
downloadFileByUrlList(fileDownloadTyp: UrlFileDownloadType, urlList: Array<string>): unknown
downloadFileForFileInfo(fileInfo: CommonFileInfo[], savePath: string): unknown
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }>
downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown
createGroupFolder(arg1: unknown, arg2: unknown): unknown
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
deleteGroupFolder(arg1: unknown, arg2: unknown): unknown
deleteTransferInfo(arg1: unknown, arg2: unknown): unknown
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown
cancelUrlDownload(arg: unknown): unknown
updateOnlineVideoElemStatus(arg: unknown): unknown
getGroupSpace(arg: unknown): unknown
getGroupFileList(groupCode: string, params: GetFileListParam): Promise<GeneralCallResult & {
groupSpaceResult: {
retCode: number
retMsg: string
clientWording: string
totalSpace: number
usedSpace: number
allUpload: boolean
}
}>
getGroupFileInfo(arg1: unknown, arg2: unknown): unknown
getGroupTransferList(arg1: unknown, arg2: unknown): unknown
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
transGroupFile(arg1: unknown, arg2: unknown): unknown
searchGroupFile(
keywords: Array<string>,
param: {
groupIds: Array<string>,
fileType: number,
context: string,
count: number,
sortType: number,
groupNames: Array<string>
}): Promise<unknown>
searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & {
transGroupFileResult: {
result: any
successFileIdList: Array<any>
failFileIdList: Array<any>
}
}>
translateEnWordToZn(words: string[]): Promise<GeneralCallResult & { words: string[] }>
getScreenOCR(path: string): Promise<unknown>
batchGetGroupFileCount(Gids: Array<string>): Promise<GeneralCallResult & { groupCodes: Array<string>, groupFileCounts: Array<number> }>
queryPicDownloadSize(arg: unknown): unknown
searchGroupFile(arg1: unknown, arg2: unknown): unknown
searchMoreGroupFile(arg: unknown): unknown
cancelSearcheGroupFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown
onlyDownloadFile(peer: Peer, arg2: unknown, arg3: Array<{
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}
>): unknown
onlyUploadFile(arg1: unknown, arg2: unknown): unknown
isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown
uploadRMFileWithoutMsg(arg: {
bizType: RMBizTypeEnum,
filePath: string,
peerUid: string,
transferId: string
useNTV2: string
}): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,128 @@
import { ChatType } from '../types'
export interface NodeIKernelSearchService {
addKernelSearchListener(...args: any[]): unknown// needs 1 arguments
removeKernelSearchListener(...args: any[]): unknown// needs 1 arguments
searchStranger(...args: any[]): unknown// needs 3 arguments
searchGroup(...args: any[]): unknown// needs 1 arguments
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown
cancelSearchLocalInfo(...args: any[]): unknown// needs 3 arguments
searchBuddyChatInfo(...args: any[]): unknown// needs 2 arguments
searchMoreBuddyChatInfo(...args: any[]): unknown// needs 1 arguments
cancelSearchBuddyChatInfo(...args: any[]): unknown// needs 3 arguments
searchContact(...args: any[]): unknown// needs 2 arguments
searchMoreContact(...args: any[]): unknown// needs 1 arguments
cancelSearchContact(...args: any[]): unknown// needs 3 arguments
searchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoSortType(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoFilterMembers(...args: any[]): unknown// needs 3 arguments
searchMoreGroupChatInfo(...args: any[]): unknown// needs 1 arguments
cancelSearchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
searchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
searchMoreChatsWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
searchChatMsgs(...args: any[]): unknown// needs 2 arguments
searchMoreChatMsgs(...args: any[]): unknown// needs 1 arguments
cancelSearchChatMsgs(...args: any[]): unknown// needs 3 arguments
searchMsgWithKeywords(...args: any[]): unknown// needs 2 arguments
searchMoreMsgWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchMsgWithKeywords(...args: any[]): unknown// needs 3 arguments
searchFileWithKeywords(keywords: string[], source: number): Promise<string>// needs 2 arguments
searchMoreFileWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchFileWithKeywords(...args: any[]): unknown// needs 3 arguments
searchAtMeChats(...args: any[]): unknown// needs 3 arguments
searchMoreAtMeChats(...args: any[]): unknown// needs 1 arguments
cancelSearchAtMeChats(...args: any[]): unknown// needs 3 arguments
searchChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
searchMoreChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
cancelSearchChatAtMeMsgs(...args: any[]): unknown// needs 3 arguments
addSearchHistory(param: {
type: number,//4
contactList: [],
id: number,//-1
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: ChatType,
buddyChatInfo: Array<{ category_name: string, peerUid: string, peerUin: string, remark: string }>,
discussChatInfo: [],
groupChatInfo: Array<
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}>,
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: string,//3
fileSize: string,
filePath: string,
fileName: string,
hits: Array<
{
start: 12,
end: 14
}
>
}
]
}): Promise<{
result: number,
errMsg: string,
id?: number
}>
removeSearchHistory(...args: any[]): unknown// needs 1 arguments
searchCache(...args: any[]): unknown// needs 3 arguments
clearSearchCache(...args: any[]): unknown// needs 1 arguments
}

View File

@@ -0,0 +1,11 @@
import { forceFetchClientKeyRetType } from './common'
export interface NodeIKernelTicketService {
addKernelTicketListener(listener: unknown): void
removeKernelTicketListener(listenerId: unknown): void
forceFetchClientKey(arg: string): Promise<forceFetchClientKeyRetType>
isNull(): boolean
}

View File

@@ -0,0 +1,19 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelTipOffService {
addKernelTipOffListener(listener: unknown): void
removeKernelTipOffListener(listenerId: unknown): void
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
getPskey(domainList: string[], nocache: boolean): Promise<GeneralCallResult & { domainPskeyMap: Map<string, string> }> //2
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
tipOffMsgs(args: unknown[]): Promise<unknown> //1
encodeUinAesInfo(args: unknown[]): Promise<unknown> //2
isNull(): boolean
}

View File

@@ -0,0 +1,5 @@
export interface NodeIKernelUixConvertService {
getUin(uid: string[]): Promise<{ uinInfo: Map<string, string> }>
getUid(uin: string[]): Promise<{ uidInfo: Map<string, string> }>
}

View File

@@ -1,2 +1,11 @@
export * from './NodeIKernelBuddyService' export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService' export * from './NodeIKernelProfileService'
export * from './NodeIKernelGroupService'
export * from './NodeIKernelProfileLikeService'
export * from './NodeIKernelMsgService'
export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService'
export * from './NodeIKernelSearchService'

View File

@@ -1,5 +1,12 @@
import { QQLevel, Sex } from './user' import { QQLevel, Sex } from './user'
export enum GroupListUpdateType {
REFRESHALL,
GETALL,
MODIFIED,
REMOVE
}
export interface Group { export interface Group {
groupCode: string groupCode: string
maxMember: number maxMember: number
@@ -38,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
@@ -53,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

@@ -1,6 +1,15 @@
import { GroupMemberRole } from './group' import { GroupMemberRole } from './group'
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
}
export enum ElementType { export enum ElementType {
UNKNOWN = 0,
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
@@ -8,20 +17,36 @@ export enum ElementType {
VIDEO = 5, VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
WALLET = 9,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10, ARK = 10,
MFACE = 11, MFACE = 11,
LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
} }
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT elementType: ElementType.TEXT
elementId: '' elementId: ''
textElement: { textElement: TextElement
content: string
atType: number
atUid: string
atTinyId: string
atNtUid: string
}
} }
export interface SendPttElement { export interface SendPttElement {
@@ -77,12 +102,7 @@ export interface SendPicElement {
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY elementType: ElementType.REPLY
elementId: '' elementId: ''
replyElement: { replyElement: ReplyElement
replayMsgSeq: string
replayMsgId: string
senderUin: string
senderUinStr: string
}
} }
export interface SendFaceElement { export interface SendFaceElement {
@@ -96,19 +116,35 @@ export interface SendMarketFaceElement {
marketFaceElement: MarketFaceElement marketFaceElement: MarketFaceElement
} }
export interface TextElement {
content: string
atType: number
atUid: string
atTinyId: string
atNtUid: string
}
export interface ReplyElement {
replayMsgSeq: string
replayMsgId: string
senderUin: string
senderUinStr: string
}
export interface FileElement { export interface FileElement {
fileMd5?: '' fileMd5?: string
fileName: string fileName: string
filePath: string filePath: string
fileSize: string fileSize: string
picHeight?: number picHeight?: number
picWidth?: number picWidth?: number
picThumbPath?: {} folderId?: string
file10MMd5?: '' picThumbPath?: Map<number, string>
fileSha?: '' file10MMd5?: string
fileSha3?: '' fileSha?: string
fileUuid?: '' fileSha3?: string
fileSubId?: '' fileUuid?: string
fileSubId?: string
thumbFileSize?: number thumbFileSize?: number
fileBizId?: number fileBizId?: number
} }
@@ -154,6 +190,50 @@ export enum ChatType {
temp = 100, temp = 100,
} }
// 来自Android分析
export enum ChatType2 {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
}
export interface PttElement { export interface PttElement {
canConvert2Text: boolean canConvert2Text: boolean
duration: number // 秒数 duration: number // 秒数
@@ -377,19 +457,23 @@ export interface RawMessage {
msgShortId?: number // 自己维护的消息id msgShortId?: number // 自己维护的消息id
msgTime: string // 时间戳,秒 msgTime: string // 时间戳,秒
msgSeq: string msgSeq: string
msgRandom: string
senderUid: string senderUid: string
senderUin?: string // 发送者QQ号 senderUin?: string // 发送者QQ号
peerUid: string // 群号 或者 QQ uid peerUid: string // 群号 或者 QQ uid
peerUin: string // 群号 或者 发送者QQ号 peerUin: string // 群号 或者 发送者QQ号
guildId: string
sendNickName: string sendNickName: string
sendMemberName?: string // 发送者群名片 sendMemberName?: string // 发送者群名片
chatType: ChatType chatType: ChatType
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[]
elements: { elements: {
elementId: string elementId: string
elementType: ElementType elementType: ElementType
replyElement: { replyElement: {
sourceMsgIdInRecords: string
senderUid: string // 原消息发送者QQ号 senderUid: string // 原消息发送者QQ号
sourceMsgIsIncPic: boolean // 原消息是否有图片 sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string sourceMsgText: string
@@ -420,3 +504,37 @@ export interface Peer {
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串 peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string guildId?: string
} }
export interface MessageElement {
elementType: ElementType
elementId: string
extBufForUI: string //"0x"
textElement?: TextElement
faceElement?: FaceElement
marketFaceElement?: MarkdownElement
replyElement?: ReplyElement
picElement?: PicElement
pttElement?: PttElement
videoElement?: VideoElement
grayTipElement?: GrayTipElement
arkElement?: ArkElement
fileElement?: FileElement
liveGiftElement?: null
markdownElement?: MarkdownElement
structLongMsgElement?: any
multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: any
walletElement?: null
inlineKeyboardElement?: InlineKeyboardElement
textGiftElement?: null //????
calendarElement?: any
yoloGameResultElement?: any
avRecordElement?: any
structMsgElement?: null
faceBubbleElement?: any
shareLocationElement?: any
tofuRecordElement?: any
taskTopMsgElement?: any
recommendedMsgElement?: any
actionBarElement?: any
}

View File

@@ -48,8 +48,28 @@ export enum GroupRequestOperateTypes {
reject = 2, reject = 2,
} }
export enum BuddyReqType {
KMEINITIATOR,
KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
}
export interface FriendRequest { export interface FriendRequest {
isInitiator?: boolean
isDecide: boolean
friendUid: string friendUid: string
reqType: BuddyReqType
reqTime: string // 时间戳,秒 reqTime: string // 时间戳,秒
extWords: string // 申请人填写的验证消息 extWords: string // 申请人填写的验证消息
isUnread: boolean isUnread: boolean
@@ -64,3 +84,41 @@ export interface FriendRequestNotify {
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }
export enum MemberExtSourceType {
DEFAULTTYPE = 0,
TITLETYPE = 1,
NEWGROUPTYPE = 2,
}
export interface GroupExtParam {
groupCode: string
seq: string
beginUin: string
dataTime: string
uinList: Array<string>
uinNum: string
groupType: string
richCardNameVer: string
sourceType: MemberExtSourceType
memberExtFilter: {
memberLevelInfoUin: number
memberLevelInfoPoint: number
memberLevelInfoActiveDay: number
memberLevelInfoLevel: number
memberLevelInfoName: number
levelName: number
dataTime: number
userShowFlag: number
sysShowFlag: number
timeToUpdate: number
nickName: number
specialTitle: number
levelNameNew: number
userShowFlagNew: number
msgNeedField: number
cmdUinFlagExt3Grocery: number
memberIcon: number
memberInfoSeq: number
}
}

View File

@@ -259,3 +259,85 @@ export interface UserDetailInfoListenerArg {
commonExt: CommonExt commonExt: CommonExt
photoWall: PhotoWall photoWall: PhotoWall
} }
export interface BuddyProfileLikeReq {
friendUids: string[]
basic: number
vote: number
favorite: number
userProfile: number
type: number
start: number
limit: number
}
export interface UserDetailInfoByUinV2 {
result: number
errMsg: string
detail: {
uid: string
uin: string
simpleInfo: SimpleInfo
commonExt: CommonExt
photoWall: null
}
}
export interface UserDetailInfoByUin {
result: number
errMsg: string
info: {
uid: string //这个没办法用
qid: string
uin: string
nick: string
remark: string
longNick: string
avatarUrl: string
birthday_year: number
birthday_month: number
birthday_day: number
sex: number //0
topTime: string
constellation: number
shengXiao: number
kBloodType: number
homeTown: string
makeFriendCareer: number
pos: string
eMail: string
phoneNum: string
college: string
country: string
province: string
city: string
postCode: string
address: string
isBlock: boolean
isSpecialCareOpen: boolean
isSpecialCareZone: boolean
ringId: string
regTime: number
interest: string
termType: number
labels: any[]
qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number }
isHideQQLevel: number
privilegeIcon: { jumpUrl: string, openIconList: any[], closeIconList: any[] }
isHidePrivilegeIcon: number
photoWall: { picList: any[] }
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean
vipLevel: number
status: number
qidianMasterFlag: number
qidianCrewFlag: number
qidianCrewFlag2: number
extStatus: number
recommendImgFlag: number
disableEmojiShortCuts: number
pendantId: string
vipNameColorId: string
}
}

View File

@@ -1,10 +1,32 @@
import { NodeIKernelBuddyService } from './services/NodeIKernelBuddyService' import {
NodeIKernelBuddyService,
NodeIKernelGroupService,
NodeIKernelProfileService,
NodeIKernelProfileLikeService,
NodeIKernelMSFService,
NodeIKernelMsgService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services'
import os from 'node:os' import os from 'node:os'
const Process = require('node:process') const Process = require('node:process')
export interface NodeIQQNTWrapperSession { export interface NodeIQQNTWrapperSession {
[key: string]: any [key: string]: any
getBuddyService(): NodeIKernelBuddyService getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService
getProfileLikeService(): NodeIKernelProfileLikeService
getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
getSearchService(): NodeIKernelSearchService
} }
export interface WrapperApi { export interface WrapperApi {
@@ -28,7 +50,7 @@ export interface WrapperConstructor {
NodeIKernelProfileListener?: any NodeIKernelProfileListener?: any
} }
export const wrapperApi: WrapperApi = {} const wrapperApi: WrapperApi = {}
export const wrapperConstructor: WrapperConstructor = {} export const wrapperConstructor: WrapperConstructor = {}
@@ -66,3 +88,7 @@ Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LA
} }
return dlopenRet return dlopenRet
} }
export function getSession() {
return wrapperApi['NodeIQQNTWrapperSession']
}

View File

@@ -1,12 +1,11 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import fs from 'fs/promises' import fsPromise from 'node:fs/promises'
import { dbUtil } from '@/common/db'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { checkFileReceived, log, sleep, uri2local } from '@/common/utils' import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api'
import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types' import { UUIDConverter } from '@/common/utils/helper'
import { FileCache } from '@/common/types' import { Peer, ChatType, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名或者fileUuid file: string // 文件名或者fileUuid
@@ -21,79 +20,60 @@ export interface GetFileResponse {
} }
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement { // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44
let element = msg.elements.find((e) => e.elementId === elementId)
if (!element) {
throw new Error('element not found')
}
return element.fileElement
}
private async download(cache: FileCache, file: string) {
log('需要调用 NTQQ 下载文件api')
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg) {
log('找到了文件 msg', msg)
let element = this.getElement(msg, cache.elementId)
log('找到了文件 element', element)
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true)
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg!, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}
}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let cache = await dbUtil.getFileCache(payload.file) const { enableLocalFile2Url } = getConfigUtil().getConfig()
if (!cache) {
throw new Error('file not found') let fileCache = await MessageUnique.getFileCacheById(String(payload.file))
if (!fileCache?.length) {
fileCache = await MessageUnique.getFileCacheByName(String(payload.file))
} }
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (cache.downloadFunc) { if (fileCache?.length) {
await cache.downloadFunc() const downloadPath = await NTQQFileApi.downloadMedia(
} fileCache[0].msgId,
try { fileCache[0].chatType,
await fs.access(cache.filePath, fs.constants.F_OK) fileCache[0].peerUid,
} catch (e) { fileCache[0].elementId,
// log("file not found", e) '',
if (cache.url) { ''
const downloadResult = await uri2local(cache.url) )
if (downloadResult.success) { const res: GetFileResponse = {
cache.filePath = downloadResult.path file: downloadPath,
dbUtil.addFileCache(payload.file, cache).then() url: downloadPath,
} else { file_size: fileCache[0].fileSize,
await this.download(cache, payload.file) file_name: fileCache[0].fileName,
}
} else {
// 没有url的可能是私聊文件或者群文件需要自己下载
await this.download(cache, payload.file)
} }
} const peer: Peer = {
let res: GetFileResponse = { chatType: fileCache[0].chatType,
file: cache.filePath, peerUid: fileCache[0].peerUid,
url: cache.url, guildId: ''
file_size: cache.fileSize, }
file_name: cache.fileName, if (fileCache[0].elementType === ElementType.PIC) {
} const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (enableLocalFile2Url) { if (msgList.msgList.length === 0) {
if (!cache.url) { throw new Error('msg not found')
}
const msg = msgList.msgList[0]
const findEle = msg.elements.find(e => e.elementId === fileCache[0].elementId)
if (!findEle) {
throw new Error('element not found')
}
res.url = await NTQQFileApi.getImageUrl(findEle.picElement)
} else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
}
if (enableLocalFile2Url && downloadPath && res.file === res.url) {
try { try {
res.base64 = await fs.readFile(cache.filePath, 'base64') res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) { } catch (e) {
throw new Error('文件下载失败. ' + e) throw new Error('文件下载失败. ' + e)
} }
} }
//不手动删除?文件持久化了
return res
} }
// if (autoDeleteFile) { throw new Error('file not found')
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res
} }
} }

View File

@@ -3,4 +3,11 @@ import { ActionName } from '../types'
export default class GetImage extends GetFileBase { export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage actionName = ActionName.GetImage
protected async _handle(payload: { file: string }) {
if (!payload.file) {
throw new Error('参数 file 不能为空')
}
return super._handle(payload)
}
} }

View File

@@ -1,24 +1,27 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { dbUtil } from '@/common/db'; import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number | string; message_id: number | string
} }
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> { export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg; actionName = ActionName.GoCQHTTP_DelEssenceMsg;
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found'); throw new Error('msg not found')
} }
return await NTQQGroupApi.removeGroupEssence( return await NTQQGroupApi.removeGroupEssence(
msg.peerUid, msg.Peer.peerUid,
msg.msgId msg.MsgId,
); )
} }
} }

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

@@ -1,8 +1,9 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import fs from 'fs' import fs from 'fs'
import { join as joinPath } from 'node:path' import fsPromise from 'fs/promises'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '../../../common/utils' import path from 'node:path'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
interface Payload { interface Payload {
@@ -22,15 +23,15 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
protected async _handle(payload: Payload): Promise<FileResponse> { protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name const isRandomName = !payload.name
let name = payload.name || randomUUID() const name = payload.name ? path.basename(payload.name) : randomUUID()
const filePath = joinPath(TEMP_DIR, name) const filePath = path.join(TEMP_DIR, name)
if (payload.base64) { if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64') await fsPromise.writeFile(filePath, payload.base64, 'base64')
} else if (payload.url) { } else if (payload.url) {
const headers = this.getHeaders(payload.headers) const headers = this.getHeaders(payload.headers)
let buffer = await httpDownload({ url: payload.url, headers: headers }) const buffer = await httpDownload({ url: payload.url, headers: headers })
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary') await fsPromise.writeFile(filePath, buffer)
} else { } else {
throw new Error('不存在任何文件, 无法下载') throw new Error('不存在任何文件, 无法下载')
} }
@@ -38,8 +39,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
if (isRandomName) { if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5 // 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath) const md5 = await calculateFileMD5(filePath)
const newPath = joinPath(TEMP_DIR, md5) const newPath = path.join(TEMP_DIR, md5)
fs.renameSync(filePath, newPath) await fsPromise.rename(filePath, newPath)
return { file: newPath } return { file: newPath }
} }
return { file: filePath } return { file: filePath }

View File

@@ -1,9 +1,9 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types' import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import { NTQQMsgApi } from '@/ntqqapi/api' import { NTQQMsgApi } from '@/ntqqapi/api'
import { dbUtil } from '../../../common/db'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: string // long msg idgocq message_id: string // long msg idgocq
@@ -14,30 +14,30 @@ interface Response {
messages: (OB11Message & { content: OB11MessageData })[] messages: (OB11Message & { content: OB11MessageData })[]
} }
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> { export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const message_id = payload.id || payload.message_id const msgId = payload.id || payload.message_id
if (!message_id) { if (!msgId) {
throw Error('message_id不能为空') throw Error('message_id不能为空')
} }
const rootMsg = await dbUtil.getMsgByLongId(message_id) const rootMsgId = MessageUnique.getShortIdByMsgId(msgId)
const rootMsg = await MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || +msgId)
if (!rootMsg) { if (!rootMsg) {
throw Error('msg not found') throw Error('msg not found')
} }
let data = await NTQQMsgApi.getMultiMsg( const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId)
{ chatType: rootMsg.chatType, peerUid: rootMsg.peerUid }, if (data?.result !== 0) {
rootMsg.msgId, throw Error('找不到相关的聊天记录' + data?.errMsg)
rootMsg.msgId,
)
if (data.result !== 0) {
throw Error('找不到相关的聊天记录' + data.errMsg)
} }
let msgList = data.msgList const msgList = data.msgList
let messages = await Promise.all( const messages = await Promise.all(
msgList.map(async (msg) => { msgList.map(async (msg) => {
let resMsg = await OB11Constructor.message(msg) const resMsg = await OB11Constructor.message(msg)
resMsg.message_id = (await dbUtil.addMsg(msg))! resMsg.message_id = MessageUnique.createMsg({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId)!
return resMsg return resMsg
}), }),
) )

View File

@@ -1,16 +1,17 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11Message, OB11User } from '../../types' import { OB11Message } from '../../types'
import { groups } from '../../../common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType } from '../../../ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
group_id: number group_id: number | string
message_seq: number message_seq?: number
count: number count?: number
reverseOrder?: boolean
} }
interface Response { interface Response {
@@ -21,23 +22,23 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const group = groups.find((group) => group.groupCode === payload.group_id.toString()) const count = payload.count || 20
if (!group) { const isReverseOrder = payload.reverseOrder || true
throw `${payload.group_id}不存在` const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[]
// 包含 message_seq 0
if (!payload.message_seq) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count)).msgList
} else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId
if (!startMsgId) throw `消息${payload.message_seq}不存在`
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList
} }
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0' if (isReverseOrder) msgList.reverse()
// log("startMsgId", startMsgId)
let msgList = (
await NTQQMsgApi.getMsgHistory(
{ chatType: ChatType.group, peerUid: group.groupCode },
startMsgId,
parseInt(payload.count?.toString()) || 20,
)
).msgList
await Promise.all( await Promise.all(
msgList.map(async (msg) => { msgList.map(async msg => {
msg.msgShortId = await dbUtil.addMsg(msg) msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
}), })
) )
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg))) const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg)))
return { messages: ob11MsgList } return { messages: ob11MsgList }

View File

@@ -1,19 +1,59 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { getUidByUin, uidMaps } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel'
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { interface Payload {
user_id: number | string
}
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> { protected async _handle(payload: Payload): Promise<OB11User> {
const user_id = payload.user_id.toString() if (!(getBuildVersion() >= 26702)) {
const uid = getUidByUin(user_id) const user_id = payload.user_id.toString()
if (!uid) { const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id)
throw new Error('查无此人') const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick,
sex: OB11UserSex.unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} else {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} }
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
} }
} }

View File

@@ -1,23 +1,26 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction'
import { ActionName } from '../types'; import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { dbUtil } from '@/common/db'; import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number | string; message_id: number | string
} }
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> { export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg; actionName = ActionName.GoCQHTTP_SetEssenceMsg;
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found'); throw new Error('msg not found')
} }
return await NTQQGroupApi.addGroupEssence( return await NTQQGroupApi.addGroupEssence(
msg.peerUid, msg.Peer.peerUid,
msg.msgId msg.MsgId
); )
} }
} }

View File

@@ -1,50 +1,70 @@
import fs from 'node:fs'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getGroup, getUidByUin } 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'
import fs from 'fs'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../msg/SendMsg'
import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
user_id: number user_id: number | string
group_id?: number group_id?: number | string
file: string file: string
name: string name: string
folder: string folder?: string
folder_id?: string
} }
class GoCQHTTPUploadFileBase extends BaseAction<Payload, null> { export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile actionName = ActionName.GoCQHTTP_UploadGroupFile
getPeer(payload: Payload): Peer {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
}
return { chatType: ChatType.group, peerUid: payload.group_id?.toString()! }
}
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
let file = payload.file let file = payload.file
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
file = `file://${file}` file = `file://${file}`
} }
const downloadResult = await uri2local(file) const downloadResult = await uri2local(file)
if (downloadResult.errMsg) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name) const sendFileEle = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id)
await NTQQMsgApi.sendMsg(this.getPeer(payload), [sendFileEle]) await sendMsg({
chatType: ChatType.group,
peerUid: payload.group_id?.toString()!,
}, [sendFileEle], [], true)
return null return null
} }
} }
export class GoCQHTTPUploadGroupFile extends GoCQHTTPUploadFileBase { export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
}
export class GoCQHTTPUploadPrivateFile extends GoCQHTTPUploadFileBase {
actionName = ActionName.GoCQHTTP_UploadPrivateFile actionName = ActionName.GoCQHTTP_UploadPrivateFile
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw `私聊${payload.user_id}不存在`
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid)
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid }
}
throw '缺少参数 user_id'
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload)
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
}
const downloadResult = await uri2local(file)
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name)
await sendMsg(peer, [sendFileEle], [], true)
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

@@ -24,7 +24,11 @@ class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
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 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,30 +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') {
group.members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString()) let _groupMembers = groupMembersArr.map(item => {
log('强制刷新群成员列表, 数量: ', group.members.length) return OB11Constructor.groupMember(payload.group_id.toString(), item)
} })
return OB11Constructor.groupMembers(group)
} else { const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
throw `${payload.group_id}不存在` const date = Math.round(Date.now() / 1000)
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

@@ -5,22 +5,19 @@ import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface Payload { interface Payload {
flag: string flag: string
// sub_type: "add" | "invite", approve?: boolean | string
// type: "add" | "invite" reason?: string
approve: boolean
reason: string
} }
export default class SetGroupAddRequest extends BaseAction<Payload, null> { export default class SetGroupAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAddRequest actionName = ActionName.SetGroupAddRequest
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString() const flag = payload.flag.toString()
const approve = payload.approve.toString() === 'true' const approve = payload.approve?.toString() !== 'false'
await NTQQGroupApi.handleGroupRequest( await NTQQGroupApi.handleGroupRequest(flag,
seq,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason, payload.reason || ''
) )
return null return null
} }

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

@@ -24,7 +24,7 @@ export default class Debug extends BaseAction<Payload, any> {
log('debug call ntqq api', payload) log('debug call ntqq api', payload)
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi] const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi]
for (const ntqqApiClass of ntqqApi) { for (const ntqqApiClass of ntqqApi) {
log('ntqqApiClass', ntqqApiClass) //log('ntqqApiClass', ntqqApiClass)
const method = ntqqApiClass[payload.method] const method = ntqqApiClass[payload.method]
if (method) { if (method) {
const result = method(...payload.args) const result = method(...payload.args)

View File

@@ -1,10 +1,8 @@
import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types' import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { uidMaps } from '../../../common/data'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { log } from '../../../common/utils/log'
interface OB11GroupRequestNotify { interface OB11GroupRequestNotify {
group_id: number group_id: number
@@ -17,11 +15,10 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> { protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await NTQQGroupApi.getGroupIgnoreNotifies() const data = await NTQQGroupApi.getGroupIgnoreNotifies()
log(data) const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE)
let notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) const returnData: OB11GroupRequestNotify[] = []
let returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = uidMaps[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin const uin = await NTQQUserApi.getUinByUid(notify.user1.uid)
returnData.push({ returnData.push({
group_id: parseInt(notify.group.groupCode), group_id: parseInt(notify.group.groupCode),
user_id: parseInt(uin), user_id: parseInt(uin),

View File

@@ -1,27 +1,24 @@
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number message_id: number | string
} }
class DeleteMsg extends BaseAction<Payload, void> { class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg actionName = ActionName.DeleteMsg
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id) if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw `消息${payload.message_id}不存在` throw `消息${payload.message_id}不存在`
} }
await NTQQMsgApi.recallMsg( await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId])
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
[msg.msgId],
)
} }
} }

View File

@@ -1,42 +1,42 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQMsgApi } from '@/ntqqapi/api' import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType, RawMessage } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { dbUtil } from '@/common/db'
import { getUidByUin } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number message_id: number | string
group_id: number group_id: number | string
user_id?: number user_id?: number | string
} }
interface Response { abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
message_id: number
}
abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
protected async getTargetPeer(payload: Payload): Promise<Peer> { protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) { if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! } const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw new Error(`无法找到私聊对象${payload.user_id}`)
}
return { chatType: ChatType.friend, peerUid }
} }
return { chatType: ChatType.group, peerUid: payload.group_id.toString() } return { chatType: ChatType.group, peerUid: payload.group_id!.toString() }
} }
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<null> {
const msg = (await dbUtil.getMsgByShortId(payload.message_id))! if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`)
}
const peer = await this.getTargetPeer(payload) const peer = await this.getTargetPeer(payload)
const sentMsg = await NTQQMsgApi.forwardMsg( const ret = await NTQQMsgApi.forwardMsg(msg.Peer, peer, [msg.MsgId])
{ if (ret.result !== 0) {
chatType: msg.chatType, throw new Error(`转发消息失败 ${ret.errMsg}`)
peerUid: msg.peerUid, }
}, return null
peer,
[msg.msgId],
)
const ob11MsgId = await dbUtil.addMsg(sentMsg)
return { message_id: ob11MsgId! }
} }
} }

View File

@@ -2,10 +2,12 @@ import { OB11Message } from '../../types'
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 { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { getMsgCache } from '@/common/data'
export interface PayloadType { export interface PayloadType {
message_id: number message_id: number | string
} }
export type ReturnDataType = OB11Message export type ReturnDataType = OB11Message
@@ -18,14 +20,22 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
if (!payload.message_id) { if (!payload.message_id) {
throw '参数message_id不能为空' throw '参数message_id不能为空'
} }
let msg = await dbUtil.getMsgByShortId(payload.message_id) const msgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString())
if (!msg) { const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(msgShortId || +payload.message_id)
msg = await dbUtil.getMsgByLongId(payload.message_id.toString()) if (!msgIdWithPeer) {
throw ('消息不存在')
} }
if (!msg) { const peer = {
throw '消息不存在' guildId: '',
peerUid: msgIdWithPeer.Peer.peerUid,
chatType: msgIdWithPeer.Peer.chatType
} }
return await OB11Constructor.message(msg) const msg = getMsgCache(msgIdWithPeer.MsgId) ?? (await NTQQMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0]
const retMsg = await OB11Constructor.message(msg)
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)!
retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id
return retMsg
} }
} }

View File

@@ -2,87 +2,52 @@ import {
AtType, AtType,
ChatType, ChatType,
ElementType, ElementType,
Friend,
Group,
GroupMemberRole, GroupMemberRole,
PicSubType,
RawMessage, RawMessage,
SendArkElement,
SendMessageElement, SendMessageElement,
} from '../../../ntqqapi/types' } from '@/ntqqapi/types'
import { friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo } from '../../../common/data' import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data'
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
OB11MessageDataType, OB11MessageDataType,
OB11MessageFile,
OB11MessageJson, OB11MessageJson,
OB11MessageMixType, OB11MessageMixType,
OB11MessageMusic, OB11MessageMusic,
OB11MessageNode, OB11MessageNode,
OB11MessageVideo,
OB11PostSendMsg, OB11PostSendMsg,
} from '../../types' } from '../../types'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg' import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName, BaseCheckResult } from '../types' import { ActionName, BaseCheckResult } from '../types'
import * as fs from 'node:fs' import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import { decodeCQCode } from '../../cqcode' import { decodeCQCode } from '../../cqcode'
import { dbUtil } from '../../../common/db' import { getConfigUtil } from '@/common/config'
import { ALLOW_SEND_TEMP_MSG, getConfigUtil } from '../../../common/config' import { log } from '@/common/utils/log'
import { log } from '../../../common/utils/log' import { sleep } from '@/common/utils/helper'
import { sleep } from '../../../common/utils/helper' import { uri2local } from '@/common/utils'
import { uri2local } from '../../../common/utils' import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
import { crychic } from '../../../ntqqapi/native/crychic' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { NTQQGroupApi } from '../../../ntqqapi/api' import { Peer } from '@/ntqqapi/types/msg'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign' import { MessageUnique } from '@/common/utils/MessageUnique'
import { Peer } from '../../../ntqqapi/types/msg' import { OB11MessageFileBase } from '../../types'
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/
return pattern.test(uri)
}
for (let msg of sendMsgList) {
if (msg['type'] && msg['data']) {
let type = msg['type']
let data = msg['data']
if (type === 'text' && !data['text']) {
return 400
}
else if (['image', 'voice', 'record'].includes(type)) {
if (!data['file']) {
return 400
}
else {
if (checkUri(data['file'])) {
return 200
}
else {
return 400
}
}
}
else if (type === 'at' && !data['qq']) {
return 400
}
else if (type === 'reply' && !data['id']) {
return 400
}
}
else {
return 400
}
}
return 200
}
export interface ReturnDataType { export interface ReturnDataType {
message_id: number message_id: number
} }
export enum ContextMode {
Normal = 0,
Private = 1,
Group = 2
}
interface MessageContext {
deleteAfterSentFiles: string[]
peer: Peer
}
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) { export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === 'string') { if (typeof message === 'string') {
if (autoEscape === true) { if (autoEscape === true) {
@@ -105,9 +70,35 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa
return message return message
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/msg/SendMsg/create-send-elements.ts#L26
async function handleOb11FileLikeMessage(
{ data: inputdata }: OB11MessageFileBase,
{ deleteAfterSentFiles }: Pick<MessageContext, 'deleteAfterSentFiles'>,
) {
//有的奇怪的框架将url作为参数 而不是file 此时优先url 同时注意可能传入的是非file://开头的目录 By Mlikiowa
const {
path,
isLocal,
fileName,
errMsg,
success,
} = (await uri2local(inputdata?.url || inputdata.file))
if (!success) {
log('文件下载失败', errMsg)
throw Error('文件下载失败' + errMsg)
}
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
return { path, fileName: inputdata.name || fileName }
}
export async function createSendElements( export async function createSendElements(
messageData: OB11MessageData[], messageData: OB11MessageData[],
target: Group | Friend | undefined, peer: Peer,
ignoreTypes: OB11MessageDataType[] = [], ignoreTypes: OB11MessageDataType[] = [],
) { ) {
let sendElements: SendMessageElement[] = [] let sendElements: SendMessageElement[] = []
@@ -125,15 +116,14 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.at: { case OB11MessageDataType.at: {
if (!target) { if (!peer) {
continue continue
} }
let atQQ = sendMsg.data?.qq if (sendMsg.data?.qq) {
if (atQQ) { const atQQ = String(sendMsg.data.qq)
atQQ = atQQ.toString()
if (atQQ === 'all') { if (atQQ === 'all') {
// todo查询剩余的at全体次数 // todo查询剩余的at全体次数
const groupCode = (target as Group)?.groupCode const groupCode = peer.peerUid
let remainAtAllCount = 1 let remainAtAllCount = 1
let isAdmin: boolean = true let isAdmin: boolean = true
if (groupCode) { if (groupCode) {
@@ -141,21 +131,28 @@ export async function createSendElements(
remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin .RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount) log(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember((target as Group)?.groupCode, selfInfo.uin) const self = await getGroupMember(groupCode, getSelfUin())
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) { } catch (e) {
} }
} }
if (isAdmin && remainAtAllCount > 0) { if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员')) sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '@全体成员'))
} }
} }
else { else if (peer.chatType === ChatType.group) {
// const atMember = group?.members.find(m => m.uin == atQQ) const atMember = await getGroupMember(peer.peerUid, atQQ)
const atMember = await getGroupMember((target as Group)?.groupCode, atQQ)
if (atMember) { if (atMember) {
const display = `@${atMember.cardName || atMember.nick}`
sendElements.push( sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick), SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, display),
)
} else {
const atNmae = sendMsg.data?.name
const uid = await NTQQUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : ''
sendElements.push(
SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display),
) )
} }
} }
@@ -163,9 +160,16 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.reply: { case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id if (sendMsg.data?.id) {
if (replyMsgId) { const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId)) if (!replyMsgId) {
log('回复消息不存在', replyMsgId)
continue
}
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId(
replyMsgId.Peer,
[replyMsgId.MsgId!]
)).msgList[0]
if (replyMsg) { if (replyMsg) {
sendElements.push( sendElements.push(
SendMsgElementConstructor.reply( SendMsgElementConstructor.reply(
@@ -197,66 +201,36 @@ export async function createSendElements(
) )
} }
break break
case OB11MessageDataType.image: case OB11MessageDataType.image: {
case OB11MessageDataType.file: const res = await SendMsgElementConstructor.pic(
case OB11MessageDataType.video: (await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })).path,
case OB11MessageDataType.voice: { sendMsg.data.summary || '',
const data = (sendMsg as OB11MessageFile).data sendMsg.data.subType || 0
let file = data.file )
const payloadFileName = data?.name deleteAfterSentFiles.push(res.picElement.sourcePath)
if (file) { sendElements.push(res)
const cache = await dbUtil.getFileCache(file) }
if (cache) { break
if (fs.existsSync(cache.filePath)) { case OB11MessageDataType.file: {
file = 'file://' + cache.filePath const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
} sendElements.push(await SendMsgElementConstructor.file(path, fileName))
else if (cache.downloadFunc) { }
await cache.downloadFunc() break
file = cache.filePath case OB11MessageDataType.video: {
} const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
else if (cache.url) { let thumb = sendMsg.data.thumb
file = cache.url if (thumb) {
} const uri2LocalRes = await uri2local(thumb)
log('找到文件缓存', file) if (uri2LocalRes.success) thumb = uri2LocalRes.path
}
const { path, isLocal, fileName, errMsg } = await uri2local(file)
if (errMsg) {
throw errMsg
}
if (path) {
if (!isLocal) {
// 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.file) {
log('发送文件', path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName))
}
else if (sendMsg.type === OB11MessageDataType.video) {
log('发送视频', path, payloadFileName || fileName)
let thumb = sendMsg.data?.thumb
if (thumb) {
let uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) {
thumb = uri2LocalRes.path
}
}
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb))
}
else if (sendMsg.type === OB11MessageDataType.voice) {
sendElements.push(await SendMsgElementConstructor.ptt(path))
}
else if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(
await SendMsgElementConstructor.pic(
path,
sendMsg.data.summary || '',
<PicSubType>parseInt(sendMsg.data?.subType?.toString()!) || 0,
),
)
}
}
} }
const res = await SendMsgElementConstructor.video(path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res)
}
break
case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.ptt(path))
} }
break break
case OB11MessageDataType.json: { case OB11MessageDataType.json: {
@@ -265,18 +239,6 @@ export async function createSendElements(
break break
case OB11MessageDataType.poke: { case OB11MessageDataType.poke: {
let qq = sendMsg.data?.qq || sendMsg.data?.id let qq = sendMsg.data?.qq || sendMsg.data?.id
if (qq) {
if ('groupCode' in target!) {
crychic.sendGroupPoke(target.groupCode, qq.toString())
}
else {
if (!qq) {
qq = parseInt(target?.uin!)
}
crychic.sendFriendPoke(qq.toString())
}
sendElements.push(SendMsgElementConstructor.poke('', '')!)
}
} }
break break
case OB11MessageDataType.dice: { case OB11MessageDataType.dice: {
@@ -332,12 +294,35 @@ export async function sendMsg(
log('设置消息超时时间', timeout) log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout) const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
log('消息发送结果', returnMsg) log('消息发送结果', returnMsg)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => { deleteAfterSentFiles.map(path => fsPromise.unlink(path))
}))
return returnMsg return returnMsg
} }
async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode): Promise<Peer> {
// This function determines the type of message by the existence of user_id / group_id,
// not message_type.
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
chatType: ChatType.group,
peerUid: payload.group_id.toString(),
}
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
const isBuddy = await NTQQFriendApi.isBuddy(Uid!)
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!,
guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
}
}
throw '请指定 group_id 或 user_id'
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> { export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
@@ -357,20 +342,12 @@ 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') {
if (!(await getFriend(payload.user_id))) { const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!ALLOW_SEND_TEMP_MSG && !(await dbUtil.getReceivedTempUinMap())[payload.user_id.toString()]) { const isBuddy = await NTQQFriendApi.isBuddy(uid!)
return { // 此处有问题
valid: false, if (!isBuddy) {
message: `不能发送临时消息`, //return { valid: false, message: '异常消息' }
}
}
} }
} }
return { return {
@@ -379,56 +356,14 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
protected async _handle(payload: OB11PostSendMsg) { protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = { const peer = await createContext(payload, ContextMode.Normal)
chatType: ChatType.friend,
peerUid: '',
}
let isTempMsg = false
let group: Group | undefined = undefined
let friend: Friend | undefined = undefined
const genGroupPeer = async () => {
group = await getGroup(payload.group_id?.toString()!)
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group?.groupCode!
}
const genFriendPeer = () => {
friend = friends.find((f) => f.uin == payload.user_id.toString())
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid
}
else {
peer.chatType = ChatType.temp
const tempUserUid = getUidByUin(payload.user_id.toString())
if (!tempUserUid) {
throw `找不到私聊对象${payload.user_id}`
}
// peer.name = tempUser.nickName
isTempMsg = true
peer.peerUid = tempUserUid
}
}
if (payload?.group_id && payload.message_type === 'group') {
await genGroupPeer()
}
else if (payload?.user_id) {
genFriendPeer()
}
else if (payload.group_id) {
await genGroupPeer()
}
else {
throw '发送消息参数错误, 请指定group_id或user_id'
}
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',
) )
if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) { if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try { try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group) const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[])
return { message_id: returnMsg?.msgShortId! } return { message_id: returnMsg?.msgShortId! }
} catch (e: any) { } catch (e: any) {
throw '发送转发消息失败 ' + e.toString() throw '发送转发消息失败 ' + e.toString()
@@ -485,15 +420,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
// log("send msg:", peer, sendElements) // log("send msg:", peer, sendElements)
const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, group || friend) const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, peer)
if (sendElements.length === 1) { if (sendElements.length === 1) {
if (sendElements[0] === null) { if (sendElements[0] === null) {
return { message_id: 0 } return { message_id: 0 }
} }
} }
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles) const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
return { message_id: returnMsg.msgShortId! } return { message_id: returnMsg.msgShortId! }
} }
@@ -521,12 +454,12 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const nodeMsg = await NTQQMsgApi.sendMsg( const nodeMsg = await NTQQMsgApi.sendMsg(
{ {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: selfInfo.uid, peerUid: getSelfUid(),
}, },
sendElements, sendElements,
true, true,
) )
await sleep(500) await sleep(400)
return nodeMsg return nodeMsg
} catch (e) { } catch (e) {
log(e, '克隆转发消息失败,将忽略本条消息', msg) log(e, '克隆转发消息失败,将忽略本条消息', msg)
@@ -534,32 +467,24 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
// 返回一个合并转发的消息id // 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = { const selfPeer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: selfInfo.uid, peerUid: getSelfUid(),
} }
let nodeMsgIds: string[] = [] let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用 // 先判断一遍是不是id和自定义混用
let needClone =
messageNodes.filter((node) => node.data.id).length && messageNodes.filter((node) => !node.data.id).length
for (const messageNode of messageNodes) { for (const messageNode of messageNodes) {
// 一个node表示一个人的消息 // 一个node表示一个人的消息
let nodeId = messageNode.data.id let nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片 // 有nodeId表示一个子转发消息卡片
if (nodeId) { if (nodeId) {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId)) const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId)
if (!needClone) { if (!nodeMsg) {
nodeMsgIds.push(nodeMsg?.msgId!) log('转发消息失败,未找到消息', nodeId)
} continue
else {
if (nodeMsg?.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg!)
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId)
}
}
} }
nodeMsgIds.push(nodeMsg.MsgId)
} }
else { else {
// 自定义的消息 // 自定义的消息
@@ -567,7 +492,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
try { try {
const { sendElements, deleteAfterSentFiles } = await createSendElements( const { sendElements, deleteAfterSentFiles } = await createSendElements(
convertMessage2List(messageNode.data.content), convertMessage2List(messageNode.data.content),
group, destPeer
) )
log('开始生成转发节点', sendElements) log('开始生成转发节点', sendElements)
let sendElementsSplit: SendMessageElement[][] = [] let sendElementsSplit: SendMessageElement[][] = []
@@ -593,7 +518,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
for (const eles of sendElementsSplit) { for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(selfPeer, eles, [], true) const nodeMsg = await sendMsg(selfPeer, eles, [], true)
nodeMsgIds.push(nodeMsg.msgId) nodeMsgIds.push(nodeMsg.msgId)
await sleep(500) await sleep(400)
log('转发节点生成成功', nodeMsg.msgId) log('转发节点生成成功', nodeMsg.msgId)
} }
deleteAfterSentFiles.map((f) => fs.unlink(f, () => { deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
@@ -605,33 +530,25 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发 // 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
let nodeMsgArray: Array<RawMessage> = [] const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer | null = null let srcPeer: Peer | null = null
let needSendSelf = false let needSendSelf = false
for (const [index, msgId] of nodeMsgIds.entries()) { for (const msgId of nodeMsgIds) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId) const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId)
if (nodeMsg) { if (nodeMsgPeer) {
nodeMsgArray.push(nodeMsg) const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]
if (!srcPeer) { srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid } if (srcPeer.peerUid !== nodeMsg.peerUid) {
}
else if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true needSendSelf = true
srcPeer = selfPeer
} }
nodeMsgArray.push(nodeMsg)
} }
} }
log('nodeMsgArray', nodeMsgArray)
nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId) nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId)
if (needSendSelf) { if (needSendSelf) {
log('需要克隆转发消息') for (const msg of nodeMsgArray) {
for (const [index, msg] of nodeMsgArray.entries()) { if (msg.peerUid === selfPeer.peerUid) continue
if (msg.peerUid !== selfInfo.uid) { await this.cloneMsg(msg)
const cloneMsg = await this.cloneMsg(msg)
if (cloneMsg) {
nodeMsgIds[index] = cloneMsg.msgId
}
}
} }
} }
// elements之间用换行符分隔 // elements之间用换行符分隔
@@ -647,50 +564,10 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeMsgIds.length === 0) { if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空') throw Error('转发消息失败,节点为空')
} }
try { const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
log('开发转发', nodeMsgIds) returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId)
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds) return returnMsg
} catch (e) {
log('forward failed', e)
return null
}
} }
// private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement {
// const musicJson = {
// app: 'com.tencent.structmsg',
// config: {
// ctime: 1709689928,
// forward: 1,
// token: '5c1e4905f926dd3a64a4bd3841460351',
// type: 'normal',
// },
// extra: { app_type: 1, appid: 100497308, uin: selfInfo.uin },
// meta: {
// news: {
// action: '',
// android_pkg_name: '',
// app_type: 1,
// appid: 100497308,
// ctime: 1709689928,
// desc: content || title,
// jumpUrl: url,
// musicUrl: audio,
// preview: image,
// source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
// source_url: '',
// tag: 'QQ音乐',
// title: title,
// uin: selfInfo.uin,
// },
// },
// prompt: content || title,
// ver: '0.0.0.1',
// view: 'news',
// }
// return SendMsgElementConstructor.ark(musicJson)
// }
} }
export default SendMsg export default SendMsg

View File

@@ -1,32 +1,36 @@
import { ActionName } from '../types' import { ActionName } from '../types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { dbUtil } from '../../../common/db' import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg' import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number message_id: number | string
emoji_id: string emoji_id: number | string
} }
export class SetMsgEmojiLike extends BaseAction<Payload, any> { export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike actionName = ActionName.SetMsgEmojiLike
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id) if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found') throw new Error('msg not found')
} }
if (!payload.emoji_id) { if (!payload.emoji_id) {
throw new Error('emojiId not found') throw new Error('emojiId not found')
} }
const msgData = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList
if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) {
throw new Error('find msg by msgid error')
}
return await NTQQMsgApi.setEmojiLike( return await NTQQMsgApi.setEmojiLike(
{ msg.Peer,
chatType: msg.chatType, msgData[0].msgSeq,
peerUid: msg.peerUid, payload.emoji_id.toString(),
}, true
msg.msgSeq,
payload.emoji_id,
true,
) )
} }
} }

View File

@@ -4,13 +4,12 @@
import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types' import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types'
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { dbUtil } from '@/common/db' import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/ntqqapi/api' import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg' import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils' import { isNull, log } from '@/common/utils'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface QuickOperationPrivateMessage { interface QuickOperationPrivateMessage {
@@ -62,16 +61,14 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc
} }
async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
msg = msg as OB11Message
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
const reply = quickAction.reply const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11 const ob11Config = getConfigUtil().getConfig().ob11
let peer: Peer = { const peer: Peer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: msg.user_id.toString(), peerUid: msg.user_id.toString(),
} }
if (msg.message_type == 'private') { if (msg.message_type == 'private') {
peer.peerUid = getUidByUin(msg.user_id.toString())! peer.peerUid = (await NTQQUserApi.getUidByUin(msg.user_id.toString()))!
if (msg.sub_type === 'group') { if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp peer.chatType = ChatType.temp
} }
@@ -81,7 +78,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
peer.peerUid = msg.group_id?.toString()! peer.peerUid = msg.group_id?.toString()!
} }
if (reply) { if (reply) {
let group: Group | null = null
let replyMessage: OB11MessageData[] = [] let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) { if (ob11Config.enableQOAutoQuote) {
replyMessage.push({ replyMessage.push({
@@ -93,7 +89,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
if (msg.message_type == 'group') { if (msg.message_type == 'group') {
group = (await getGroup(msg.group_id?.toString()!))!
if ((quickAction as QuickOperationGroupMessage).at_sender) { if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({ replyMessage.push({
type: 'at', type: 'at',
@@ -104,23 +99,26 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
} }
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!) const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, peer)
log(`发送消息给`, peer, sendElements)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log) sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log)
} }
if (msg.message_type === 'group') { if (msg.message_type === 'group') {
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
const rawMessage = await MessageUnique.getMsgIdAndPeerByShortId(+(msg.message_id ?? 0))
if (!rawMessage) return
// handle group msg // handle group msg
if (groupMsgQuickAction.delete) { if (groupMsgQuickAction.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage?.msgId!]).then().catch(log) NTQQMsgApi.recallMsg(peer, [rawMessage.MsgId]).then().catch(log)
} }
if (groupMsgQuickAction.kick) { if (groupMsgQuickAction.kick) {
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage?.senderUid!]).then().catch(log) const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
NTQQGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).then().catch(log)
} }
if (groupMsgQuickAction.ban) { if (groupMsgQuickAction.ban) {
const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
NTQQGroupApi.banMember(peer.peerUid, [ NTQQGroupApi.banMember(peer.peerUid, [
{ {
uid: rawMessage?.senderUid!, uid: msgList[0].senderUid,
timeStamp: groupMsgQuickAction.ban_duration || 60 * 30, timeStamp: groupMsgQuickAction.ban_duration || 60 * 30,
}, },
]).then().catch(log) ]).then().catch(log)

View File

@@ -1,6 +1,6 @@
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { selfInfo } from '../../../common/data' import { getSelfInfo, getSelfNick } from '../../../common/data'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
@@ -8,7 +8,10 @@ class GetLoginInfo extends BaseAction<null, OB11User> {
actionName = ActionName.GetLoginInfo actionName = ActionName.GetLoginInfo
protected async _handle(payload: null) { protected async _handle(payload: null) {
return OB11Constructor.selfInfo(selfInfo) return OB11Constructor.selfInfo({
...getSelfInfo(),
nick: await getSelfNick(true)
})
} }
} }

View File

@@ -1,14 +1,14 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11Status } from '../../types' import { OB11Status } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { selfInfo } from '../../../common/data' import { getSelfInfo } from '../../../common/data'
export default class GetStatus extends BaseAction<any, OB11Status> { export default class GetStatus extends BaseAction<any, OB11Status> {
actionName = ActionName.GetStatus actionName = ActionName.GetStatus
protected async _handle(payload: any): Promise<OB11Status> { protected async _handle(payload: any): Promise<OB11Status> {
return { return {
online: selfInfo.online!, online: getSelfInfo().online!,
good: true, good: 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

@@ -1,16 +1,24 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQUserApi } from '@/ntqqapi/api' import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Response {
cookies: string
bkn: string
}
interface Payload { interface Payload {
domain: string domain: string
} }
export class GetCookies extends BaseAction<Payload, { cookies: string; bkn: string }> { export class GetCookies extends BaseAction<Payload, Response> {
actionName = ActionName.GetCookies actionName = ActionName.GetCookies
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const domain = payload.domain || 'qun.qq.com' const cookiesObject = await NTQQUserApi.getCookies(payload.domain)
return NTQQUserApi.getCookies(domain); //把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const bkn = WebApi.genBkn(cookiesObject.p_skey)
return { cookies, bkn }
} }
} }

View File

@@ -1,11 +1,10 @@
import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { friends, rawFriends } from '@/common/data' import { friends } from '@/common/data'
import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQFriendApi } from '@/ntqqapi/api' import { NTQQFriendApi } from '@/ntqqapi/api'
import { CategoryFriend } from '@/ntqqapi/types' import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -15,7 +14,7 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList actionName = ActionName.GetFriendList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (+qqPkgInfo.buildVersion >= 26702) { if (getBuildVersion() >= 26702) {
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true')) return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true'))
} }
if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
@@ -30,11 +29,16 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
} }
} }
// extend
export class GetFriendWithCategory extends BaseAction<void, Array<CategoryFriend>> { export class GetFriendWithCategory extends BaseAction<void, any> {
actionName = ActionName.GetFriendsWithCategory; actionName = ActionName.GetFriendsWithCategory;
protected async _handle(payload: void) { protected async _handle(payload: void) {
return rawFriends; if (getBuildVersion() >= 26702) {
//全新逻辑
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2ExWithCate(true))
} else {
throw new Error('this ntqq version not support, must be 26702 or later')
}
} }
} }

View File

@@ -1,8 +1,6 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getFriend, getUidByUin, uidMaps } from '../../../common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQFriendApi } from '../../../ntqqapi/api/friend' import { NTQQUserApi } from '@/ntqqapi/api'
import { log } from '../../../common/utils/log'
interface Payload { interface Payload {
user_id: number user_id: number
@@ -13,19 +11,12 @@ export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
log('点赞参数', payload)
try { try {
const qq = payload.user_id.toString() const qq = payload.user_id.toString()
const friend = await getFriend(qq) const uid: string = await NTQQUserApi.getUidByUin(qq) || ''
let uid: string const result = await NTQQUserApi.like(uid, parseInt(payload.times?.toString()) || 1)
if (!friend) {
uid = getUidByUin(qq)!
} else {
uid = friend.uid
}
let result = await NTQQFriendApi.likeFriend(uid, parseInt(payload.times?.toString()) || 1)
if (result.result !== 0) { if (result.result !== 0) {
throw result.errMsg throw Error(result.errMsg)
} }
} catch (e) { } catch (e) {
throw `点赞失败 ${e}` throw `点赞失败 ${e}`

View File

@@ -4,7 +4,7 @@ import { NTQQFriendApi } from '../../../ntqqapi/api/friend'
interface Payload { interface Payload {
flag: string flag: string
approve: boolean approve?: boolean | string
remark?: string remark?: string
} }
@@ -12,7 +12,7 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest actionName = ActionName.SetFriendAddRequest
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const approve = payload.approve.toString() === 'true' const approve = payload.approve?.toString() !== 'false'
await NTQQFriendApi.handleFriendRequest(payload.flag, approve) await NTQQFriendApi.handleFriendRequest(payload.flag, approve)
return null return null
} }

View File

@@ -17,26 +17,22 @@ import {
Group, Group,
Peer, Peer,
GroupMember, GroupMember,
PicType,
RawMessage, RawMessage,
SelfInfo, SelfInfo,
Sex, Sex,
TipGroupElementType, TipGroupElementType,
User, User,
VideoElement, FriendV2,
FriendV2 ChatType2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } 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 { dbUtil } from '../common/db' import { MessageUnique } from '../common/utils/MessageUnique'
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'
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent' import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQFileApi } from '../ntqqapi/api/file'
import { NTQQMsgApi } from '../ntqqapi/api/msg'
import { calcQQLevel } from '../common/utils/qqlevel' import { calcQQLevel } from '../common/utils/qqlevel'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { isNull, sleep } from '../common/utils/helper' import { isNull, sleep } from '../common/utils/helper'
@@ -44,7 +40,7 @@ import { getConfigUtil } from '../common/config'
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent' import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent'
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent'
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent' import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { NTQQGroupApi } from '../ntqqapi/api' import { NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQMsgApi } from '../ntqqapi/api'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { mFaceCache } from '../ntqqapi/constructor' import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
@@ -54,8 +50,6 @@ import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11Poke
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent'
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent' import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent'
let lastRKeyUpdateTime = 0
export class OB11Constructor { export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { static async message(msg: RawMessage): Promise<OB11Message> {
let config = getConfigUtil().getConfig() let config = getConfigUtil().getConfig()
@@ -64,15 +58,15 @@ export class OB11Constructor {
debug, debug,
ob11: { messagePostFormat }, ob11: { messagePostFormat },
} = config } = config
const message_type = msg.chatType == ChatType.group ? 'group' : 'private' const selfUin = getSelfUin()
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin), self_id: parseInt(selfUin),
user_id: parseInt(msg.senderUin!), user_id: parseInt(msg.senderUin!),
time: parseInt(msg.msgTime) || Date.now(), time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId!, message_id: msg.msgShortId!,
real_id: msg.msgShortId!, real_id: msg.msgShortId!,
message_seq: msg.msgShortId!, message_seq: msg.msgShortId!,
message_type: msg.chatType == ChatType.group ? 'group' : 'private', message_type: msg.chatType === ChatType.group ? 'group' : 'private',
sender: { sender: {
user_id: parseInt(msg.senderUin!), user_id: parseInt(msg.senderUin!),
nickname: msg.sendNickName, nickname: msg.sendNickName,
@@ -83,7 +77,7 @@ export class OB11Constructor {
sub_type: 'friend', sub_type: 'friend',
message: messagePostFormat === 'string' ? '' : [], message: messagePostFormat === 'string' ? '' : [],
message_format: messagePostFormat === 'string' ? 'string' : 'array', message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE, post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
} }
if (debug) { if (debug) {
resMsg.raw = msg resMsg.raw = msg
@@ -99,16 +93,17 @@ export class OB11Constructor {
} }
else if (msg.chatType == ChatType.friend) { else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = 'friend' resMsg.sub_type = 'friend'
const friend = await getFriend(msg.senderUin!) resMsg.sender.nickname = (await NTQQUserApi.getUserDetailInfo(msg.senderUid)).nick
if (friend) {
resMsg.sender.nickname = friend.nick
}
} }
else if (msg.chatType == ChatType.temp) { else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group' resMsg.sub_type = 'group'
const tempGroupCode = tempGroupCodeMap[msg.peerUin] const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid)
if (tempGroupCode) { if (ret.result === 0) {
resMsg.group_id = parseInt(tempGroupCode) resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
} else {
resMsg.group_id = 284840486 //兜底数据
resMsg.sender.nickname = '临时会话'
} }
} }
@@ -155,115 +150,121 @@ export class OB11Constructor {
} }
else if (element.replyElement) { else if (element.replyElement) {
message_data['type'] = OB11MessageDataType.reply message_data['type'] = OB11MessageDataType.reply
// log("收到回复消息", element.replyElement.replayMsgSeq)
try { try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq) const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId) if (!records) throw new Error('找不到回复消息')
if (replyMsg) { let replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({
message_data['data']['id'] = replyMsg.msgShortId?.toString() peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, element.replyElement.replayMsgSeq, 1, true, true)).msgList[0]
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
}
replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replyElement.replayMsgSeq)).msgList[0]
} }
else { // 284840486: 合并消息内侧 消息具体定位不到
continue if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') {
throw new Error('回复消息消息验证失败')
} }
message_data['data']['id'] = MessageUnique.createMsg({
peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, replyMsg.msgId)?.toString()
} catch (e: any) { } catch (e: any) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
continue
} }
} }
else if (element.picElement) { else if (element.picElement) {
message_data['type'] = OB11MessageDataType.image message_data['type'] = OB11MessageDataType.image
// message_data["data"]["file"] = element.picElement.sourcePath const { picElement } = element
let fileName = element.picElement.fileName /*let fileName = picElement.fileName
const sourcePath = element.picElement.sourcePath const isGif = picElement.picType === PicType.gif
const isGif = element.picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) { if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif' fileName += '.gif'
} }*/
message_data['data']['file'] = fileName message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = element.picElement.picSubType message_data['data']['subType'] = picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath //message_data['data']['file_id'] = picElement.fileUuid
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement)
message_data['data']['file_size'] = picElement.fileSize
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement, msg.chatType) MessageUnique.addFileCache({
// message_data["data"]["file_id"] = element.picElement.fileUuid peerUid: msg.peerUid,
message_data['data']['file_size'] = element.picElement.fileSize msgId: msg.msgId,
dbUtil msgTime: +msg.msgTime,
.addFileCache(fileName, { chatType: msg.chatType,
fileName, elementId: element.elementId,
elementId: element.elementId, elementType: element.elementType,
filePath: sourcePath, fileName: picElement.fileName,
fileSize: element.picElement.fileSize.toString(), fileSize: String(picElement.fileSize || '0'),
url: message_data['data']['url'], fileUuid: picElement.fileUuid
downloadFunc: async () => { })
await NTQQFileApi.downloadMedia(
msg.msgId,
msg.chatType,
msg.peerUid,
element.elementId,
element.picElement.thumbPath?.get(0) || '',
element.picElement.sourcePath,
)
},
}).then()
} }
else if (element.videoElement || element.fileElement) { else if (element.videoElement) {
const videoOrFileElement = element.videoElement || element.fileElement message_data['type'] = OB11MessageDataType.video
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file const { videoElement } = element
message_data['type'] = ob11MessageDataType message_data['data']['file'] = videoElement.fileName
message_data['data']['file'] = videoOrFileElement.fileName message_data['data']['path'] = videoElement.filePath
message_data['data']['path'] = videoOrFileElement.filePath //message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_id'] = videoOrFileElement.fileUuid message_data['data']['file_size'] = videoElement.fileSize
message_data['data']['file_size'] = videoOrFileElement.fileSize message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
if (element.videoElement) { chatType: msg.chatType,
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({ peerUid: msg.peerUid,
chatType: msg.chatType, }, msg.msgId, element.elementId)
peerUid: msg.peerUid, MessageUnique.addFileCache({
}, msg.msgId, element.elementId, peerUid: msg.peerUid,
) msgId: msg.msgId,
} msgTime: +msg.msgTime,
dbUtil chatType: msg.chatType,
.addFileCache(videoOrFileElement.fileUuid!, { elementId: element.elementId,
msgId: msg.msgId, elementType: element.elementType,
elementId: element.elementId, fileName: videoElement.fileName,
fileName: videoOrFileElement.fileName, fileSize: String(videoElement.fileSize || '0'),
filePath: videoOrFileElement.filePath, fileUuid: videoElement.fileUuid!
fileSize: videoOrFileElement.fileSize!, })
downloadFunc: async () => { }
await NTQQFileApi.downloadMedia( else if (element.fileElement) {
msg.msgId, message_data['type'] = OB11MessageDataType.file
msg.chatType, const { fileElement } = element
msg.peerUid, message_data['data']['file'] = fileElement.fileName
element.elementId, message_data['data']['path'] = fileElement.filePath
ob11MessageDataType == OB11MessageDataType.video message_data['data']['file_id'] = fileElement.fileUuid
? (videoOrFileElement as VideoElement).thumbPath?.get(0) message_data['data']['file_size'] = fileElement.fileSize
: null, MessageUnique.addFileCache({
videoOrFileElement.filePath, peerUid: msg.peerUid,
) msgId: msg.msgId,
}, msgTime: +msg.msgTime,
}) chatType: msg.chatType,
.then() elementId: element.elementId,
// 怎么拿到url呢 elementType: element.elementType,
fileName: fileElement.fileName,
fileSize: String(fileElement.fileSize || '0'),
fileUuid: fileElement.fileUuid!
})
} }
else if (element.pttElement) { else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice message_data['type'] = OB11MessageDataType.voice
message_data['data']['file'] = element.pttElement.fileName const { pttElement } = element
message_data['data']['path'] = element.pttElement.filePath message_data['data']['file'] = pttElement.fileName
// message_data["data"]["file_id"] = element.pttElement.fileUuid message_data['data']['path'] = pttElement.filePath
message_data['data']['file_size'] = element.pttElement.fileSize //message_data['data']['file_id'] = pttElement.fileUuid
dbUtil message_data['data']['file_size'] = pttElement.fileSize
.addFileCache(element.pttElement.fileName, { MessageUnique.addFileCache({
elementId: element.elementId, peerUid: msg.peerUid,
fileName: element.pttElement.fileName, msgId: msg.msgId,
filePath: element.pttElement.filePath, msgTime: +msg.msgTime,
fileSize: element.pttElement.fileSize, chatType: msg.chatType,
}) elementId: element.elementId,
.then() elementType: element.elementType,
fileName: pttElement.fileName,
// log("收到语音消息", msg) fileSize: String(pttElement.fileSize || '0'),
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { fileUuid: pttElement.fileUuid
// console.log("语音转文字结果", text) })
// }).catch(err => {
// console.log("语音转文字失败", err)
// })
} }
else if (element.arkElement) { else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json message_data['type'] = OB11MessageDataType.json
@@ -336,7 +337,11 @@ export class OB11Constructor {
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) return new OB11FriendPokeEvent(
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
} }
} }
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE //下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
@@ -373,7 +378,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)
@@ -421,24 +426,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()
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(selfInfo.uin), parseInt(getSelfUin()),
parseInt(adminUin), parseInt(adminUin),
'kick_me', 'kick_me'
) )
} }
} catch (e) { } catch (e) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave') return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(getSelfUin()),
0,
'leave'
)
} }
} }
} }
@@ -472,16 +479,27 @@ export class OB11Constructor {
const senderUin = emojiLikeData.gtip.qq.jp const senderUin = emojiLikeData.gtip.qq.jp
const msgSeq = emojiLikeData.gtip.url.msgseq const msgSeq = emojiLikeData.gtip.url.msgseq
const emojiId = emojiLikeData.gtip.face.id const emojiId = emojiLikeData.gtip.face.id
const msg = await dbUtil.getMsgBySeqId(msgSeq) const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({
if (!msg) { chatType: ChatType.group,
guildId: '',
peerUid: msg.peerUid,
}, msgSeq, 1, true, true)).msgList
if (replyMsgList.length < 1) {
return return
} }
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId!, [ const likes = [
{ {
emoji_id: emojiId, emoji_id: emojiId,
count: 1, count: 1,
}, },
]) ]
const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid),
parseInt(senderUin),
shortId!,
likes
)
} catch (e: any) { } catch (e: any) {
log('解析表情回应消息失败', e.stack) log('解析表情回应消息失败', e.stack)
} }
@@ -541,7 +559,12 @@ export class OB11Constructor {
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) return new OB11GroupPokeEvent(
parseInt(msg.peerUid),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
} }
} }
if (grayTipElement.jsonGrayTipElement.busiId == 2401) { if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
@@ -549,20 +572,23 @@ export class OB11Constructor {
const searchParams = new URL(json.items[0].jp).searchParams const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')! const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode') const Group = searchParams.get('groupCode')
const Businessid = searchParams.get('businessid')
const Peer: Peer = { const Peer: Peer = {
guildId: '', guildId: '',
chatType: ChatType.group, chatType: ChatType.group,
peerUid: Group! peerUid: Group!
} }
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList const { msgList } = await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)
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可能是 历史消息 或 自身消息
if (msgList[0].senderUin === '0') { //if (msgList[0].senderUin === '0') {
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin //msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin()
} //}
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin)) return new OB11GroupEssenceEvent(
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgList[0].msgId)!,
parseInt(msgList[0].senderUin!)
)
// 获取MsgSeq+Peer可获取具体消息 // 获取MsgSeq+Peer可获取具体消息
} }
if (grayTipElement.jsonGrayTipElement.busiId == 2407) { if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
@@ -583,27 +609,26 @@ export class OB11Constructor {
static async RecallEvent( static async RecallEvent(
msg: RawMessage, msg: RawMessage,
shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> { ): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
let msgElement = msg.elements.find( const msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL, (element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL,
) )
if (!msgElement) { if (!msgElement) {
return return
} }
const isGroup = msg.chatType === ChatType.group
const revokeElement = msgElement.grayTipElement.revokeElement const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) { if (msg.chatType === ChatType.group) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid) const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!)
return new OB11GroupRecallNoticeEvent( return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(sender?.uin!), parseInt(msg.senderUin!),
parseInt(operator?.uin!), parseInt(operator?.uin || msg.senderUin!),
msg.msgShortId!, shortId,
) )
} }
else { else {
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!) return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), shortId)
} }
} }
@@ -673,7 +698,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

@@ -1,4 +1,4 @@
import { selfInfo } from '../../common/data' import { getSelfUin } from '../../common/data'
export enum EventType { export enum EventType {
META = 'meta_event', META = 'meta_event',
@@ -10,6 +10,6 @@ export enum EventType {
export abstract class OB11BaseEvent { export abstract class OB11BaseEvent {
time = Math.floor(Date.now() / 1000) time = Math.floor(Date.now() / 1000)
self_id = parseInt(selfInfo.uin) self_id = parseInt(getSelfUin())
abstract post_type: EventType abstract post_type: EventType
} }

View File

@@ -1,11 +1,11 @@
import { Response } from 'express' import { Response } from 'express'
import { OB11Response } from '../action/OB11Response' import { OB11Response } from '../action/OB11Response'
import { HttpServerBase } from '@/common/server/http' import { HttpServerBase } from '@/common/server/http'
import { actionHandlers, actionMap } from '../action' import { actionMap } from '../action'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
import { postOb11Event } from './post-ob11-event' import { postOb11Event } from './post-ob11-event'
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
import { selfInfo } from '@/common/data' import { getSelfInfo } from '@/common/data'
class OB11HTTPServer extends HttpServerBase { class OB11HTTPServer extends HttpServerBase {
name = 'LLOneBot server' name = 'LLOneBot server'
@@ -40,7 +40,7 @@ class HTTPHeart {
} }
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
// ws的心跳是ws自己维护的 // ws的心跳是ws自己维护的
postOb11Event(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!), false, false) postOb11Event(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!), false, false)
}, heartInterval) }, heartInterval)
} }

View File

@@ -1,5 +1,5 @@
import { OB11Message } from '../types' import { OB11Message } from '../types'
import { selfInfo } from '@/common/data' import { getSelfUin } from '@/common/data'
import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent' import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent'
import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent' import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent'
import { WebSocket as WebSocketClass } from 'ws' import { WebSocket as WebSocketClass } from 'ws'
@@ -35,9 +35,10 @@ export function postWsEvent(event: PostEventType) {
export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = true) { export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = true) {
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
const selfUin = getSelfUin()
// 判断msg是否是event // 判断msg是否是event
if (!config.reportSelfMessage && !reportSelf) { if (!config.reportSelfMessage && !reportSelf) {
if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfInfo.uin) { if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfUin) {
return return
} }
} }
@@ -48,7 +49,7 @@ export function postOb11Event(msg: PostEventType, reportSelf = false, postWs = t
const sig = hmac.digest('hex') const sig = hmac.digest('hex')
let headers = { let headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-self-id': selfInfo.uin, 'x-self-id': selfUin,
} }
if (config.ob11.httpSecret) { if (config.ob11.httpSecret) {
headers['x-signature'] = 'sha1=' + sig headers['x-signature'] = 'sha1=' + sig

View File

@@ -1,4 +1,4 @@
import { selfInfo } from '../../../common/data' import { getSelfInfo } from '../../../common/data'
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent' import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent'
import { ActionName } from '../../action/types' import { ActionName } from '../../action/types'
import { OB11Response } from '../../action/OB11Response' import { OB11Response } from '../../action/OB11Response'
@@ -78,6 +78,7 @@ export class ReverseWebsocket {
private connect() { private connect() {
const { token, heartInterval } = getConfigUtil().getConfig() const { token, heartInterval } = getConfigUtil().getConfig()
const selfInfo = getSelfInfo()
this.websocket = new WebSocketClass(this.url, { this.websocket = new WebSocketClass(this.url, {
maxPayload: 1024 * 1024 * 1024, maxPayload: 1024 * 1024 * 1024,
handshakeTimeout: 2000, handshakeTimeout: 2000,

View File

@@ -9,7 +9,7 @@ import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent'
import { WebsocketServerBase } from '../../../common/server/websocket' 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 { selfInfo } 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'
@@ -59,7 +59,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
} }
const { heartInterval } = getConfigUtil().getConfig() const { heartInterval } = getConfigUtil().getConfig()
const wsClientInterval = setInterval(() => { const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!)) postWsEvent(new OB11HeartbeatEvent(getSelfInfo().online!, true, heartInterval!))
}, heartInterval) // 心跳包 }, heartInterval) // 心跳包
wsClient.on('close', () => { wsClient.on('close', () => {
log('event上报ws客户端已断开') log('event上报ws客户端已断开')

View File

@@ -6,12 +6,12 @@ import { isNull } from '../../../common/utils/helper'
export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) { export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) {
try { try {
let packet = Object.assign({}, data) const packet = Object.assign({}, data)
if (isNull(packet['echo'])) { if (isNull(packet['echo'])) {
delete packet['echo'] delete packet['echo']
} }
wsClient.send(JSON.stringify(packet)) wsClient.send(JSON.stringify(packet))
log('ws 消息上报', wsClient.url || '', data) //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 {
@@ -164,7 +165,7 @@ export interface OB11MessagePoke {
} }
} }
interface OB11MessageFileBase { export interface OB11MessageFileBase {
data: { data: {
thumb?: string thumb?: string
name?: string name?: string

View File

@@ -1,4 +1,3 @@
/// <reference path="../global.d.ts" />
import { CheckVersion } from '../common/types' import { CheckVersion } from '../common/types'
import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components' import { SettingButton, SettingItem, SettingList, SettingSwitch, SettingSelect } from './components'
// @ts-ignore // @ts-ignore
@@ -26,22 +25,24 @@ function initSideBar() {
}) })
} }
function isEmpty(value: unknown) {
return value === undefined || value === null || value === ''
}
async function onSettingWindowCreated(view: Element) { async function onSettingWindowCreated(view: Element) {
window.llonebot.log('setting window created') window.llonebot.log('setting window created')
initSideBar() initSideBar()
const isEmpty = (value: any) => value === undefined || value === null || value === ''
let config = await window.llonebot.getConfig() let config = await window.llonebot.getConfig()
let ob11Config = { ...config.ob11 } let ob11Config = { ...config.ob11 }
const setConfig = (key: string, value: any) => { const setConfig = (key: string, value: any) => {
const configKey = key.split('.') const configKey = key.split('.')
if (key.indexOf('ob11') === 0) { if (key.indexOf('ob11') === 0) {
if (configKey.length === 2) ob11Config[configKey[1]] = value if (configKey.length === 2) ob11Config[configKey[1]] = value
else ob11Config[key] = value else ob11Config[key] = value
} else { } else {
if (configKey.length === 2) config[configKey[0]][configKey[1]] = value if (configKey.length === 2) config[configKey[0]][configKey[1]] = value
else config[key] = value else config[key] = value
if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) { if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) {
window.llonebot.setConfig(false, config) window.llonebot.setConfig(false, config)
} }
@@ -65,7 +66,7 @@ async function onSettingWindowCreated(view: Element) {
]), ]),
SettingList([ SettingList([
SettingItem( SettingItem(
'是否启用 LLOneBot, 重启QQ后生效', '是否启用 LLOneBot, 重启 QQ 后生效',
null, null,
SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }), SettingSwitch('enableLLOB', config.enableLLOB, { 'control-display-id': 'config-enableLLOB' }),
)] )]
@@ -160,21 +161,21 @@ async function onSettingWindowCreated(view: Element) {
SettingSelect( SettingSelect(
[ [
{ text: '消息段', value: 'array' }, { text: '消息段', value: 'array' },
{ text: 'CQ码', value: 'string' }, { text: 'CQ 码', value: 'string' },
], ],
'ob11.messagePostFormat', 'ob11.messagePostFormat',
config.ob11.messagePostFormat, config.ob11.messagePostFormat,
), ),
), ),
SettingItem( SettingItem(
'ffmpeg 路径,发送语音、视频需要同时保证ffprobe和ffmpeg在一起', 'FFmpeg 路径,发送语音、视频需要',
` <a href="javascript:LiteLoader.api.openExternal(\'https://llonebot.github.io/zh-CN/guide/ffmpeg\');">下载地址</a> <span id="config-ffmpeg-path-text">, 路径:${ `<a href="javascript:LiteLoader.api.openExternal(\'https://llonebot.github.io/zh-CN/guide/ffmpeg\');">可点此下载</a>, 路径: <span id="config-ffmpeg-path-text">${
!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定' !isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'
}</span>`, }</span>, 需保证 FFprobe 和 FFmpeg 在一起`,
SettingButton('选择ffmpeg', 'config-ffmpeg-select'), SettingButton('选择 FFmpeg', 'config-ffmpeg-select'),
), ),
SettingItem( SettingItem(
'音乐卡片签名地址', '音乐卡片签名 URL 地址',
null, null,
`<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="musicSignUrl" type="text" value="${config.musicSignUrl}" placeholder="未设置" /></div>`, `<div class="q-input" style="width:210px;"><input class="q-input__inner" data-config-key="musicSignUrl" type="text" value="${config.musicSignUrl}" placeholder="未设置" /></div>`,
'config-musicSignUrl', 'config-musicSignUrl',
@@ -218,6 +219,11 @@ async function onSettingWindowCreated(view: Element) {
`${window.LiteLoader.plugins['LLOneBot'].path.data}/logs`, `${window.LiteLoader.plugins['LLOneBot'].path.data}/logs`,
SettingButton('打开', 'config-open-log-path'), SettingButton('打开', 'config-open-log-path'),
), ),
SettingItem(
'消息内容缓存时长',
'单位为秒,可用于获取撤回的消息',
`<div class="q-input"><input class="q-input__inner" data-config-key="msgCacheExpire" type="number" min="1" value="${config.msgCacheExpire}" placeholder="${config.msgCacheExpire}" /></div>`,
),
]), ]),
SettingList([ SettingList([
SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')), SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')),
@@ -410,10 +416,7 @@ async function onSettingWindowCreated(view: Element) {
buttonDom.addEventListener('click', async () => { buttonDom.addEventListener('click', async () => {
window.llonebot.checkVersion().then(checkVersionFunc) window.llonebot.checkVersion().then(checkVersionFunc)
}) })
} else if (!ResultVersion.result) {
return
}
if (!ResultVersion.result) {
titleDom.innerHTML = '当前已是最新版本 v' + version titleDom.innerHTML = '当前已是最新版本 v' + version
buttonDom.innerHTML = '无需更新' buttonDom.innerHTML = '无需更新'
} else { } else {

View File

@@ -1 +1 @@
export const version = '3.28.0' export const version = '3.29.3'