Compare commits

...

65 Commits

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

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

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

View File

@@ -1,9 +1,9 @@
# LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发
> [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**
TG群<https://t.me/+nLZEnpne-pQ1OWFl>
@@ -29,6 +29,7 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 鸣谢
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [chronocat](https://github.com/chrononeko/chronocat)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"name": "llonebot",
"version": "1.0.0",
"type": "module",
"description": "NTQQLiteLoaderOneBotApi",
"description": "",
"main": "dist/main.js",
"scripts": {
"build": "electron-vite build",
@@ -16,13 +16,16 @@
"author": "",
"license": "MIT",
"dependencies": {
"@minatojs/driver-sqlite": "^4.5.0",
"compressing": "^1.10.1",
"cordis": "^3.18.0",
"cors": "^2.8.5",
"cosmokit": "^1.6.2",
"express": "^4.19.2",
"fast-xml-parser": "^4.4.1",
"file-type": "^19.4.0",
"file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.3",
"level": "^8.0.1",
"minato": "^3.5.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
},
@@ -32,10 +35,10 @@
"@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.14.15",
"@types/ws": "^8.5.12",
"electron": "^29.1.4",
"electron": "^31.4.0",
"electron-vite": "^2.3.0",
"typescript": "^5.5.4",
"vite": "^5.4.0",
"vite": "^5.4.2",
"vite-plugin-cp": "^4.0.8"
},
"packageManager": "yarn@4.4.0"

View File

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

View File

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

View File

@@ -5,9 +5,7 @@ import path from 'node:path'
import { getSelfUin } from './data'
import { DATA_DIR } from './utils'
export const HOOK_LOG = false
export const ALLOW_SEND_TEMP_MSG = false
//export const HOOK_LOG = false
export class ConfigUtil {
private readonly configPath: string
@@ -52,6 +50,7 @@ export class ConfigUtil {
autoDeleteFile: false,
autoDeleteFileSecond: 60,
musicSignUrl: '',
msgCacheExpire: 120
}
if (!fs.existsSync(this.configPath)) {

View File

@@ -1,6 +1,5 @@
import {
type Friend,
type Group,
type GroupMember,
type SelfInfo,
} from '../ntqqapi/types'
@@ -9,23 +8,25 @@ import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log'
import { isNumeric } from './utils/helper'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
import { getBuildVersion } from './utils/QQBasicInfo'
export let groups: Group[] = []
export let friends: Friend[] = []
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
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> {
let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
let filterValue = uinOrUid
const filterKey: 'uin' | 'uid' = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
const filterValue = uinOrUid
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend) {
if (!friend && getBuildVersion() < 26702) {
try {
const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
@@ -39,39 +40,15 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
return friend
}
export async function getGroup(qq: string): Promise<Group | undefined> {
let group = groups.find((group) => group.groupCode === qq.toString())
if (!group) {
try {
const _groups = await NTQQGroupApi.getGroups(true)
group = _groups.find((group) => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
}
return group
}
export function deleteGroup(groupCode: string) {
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString())
// log(groups, groupCode, groupIndex);
if (groupIndex !== -1) {
log('删除群', groupCode)
groups.splice(groupIndex, 1)
}
}
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
let members = groupMembers.get(groupQQ)
export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString()
const memberUinOrUidStr = memberUinOrUid.toString()
let members = groupMembers.get(groupCodeStr)
if (!members) {
try {
members = await NTQQGroupApi.getGroupMembers(groupQQ)
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表
groupMembers.set(groupQQ, members)
groupMembers.set(groupCodeStr, members)
}
catch (e) {
return null
@@ -79,16 +56,17 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s
}
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUid)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUid)
if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
} else {
member = members!.get(memberUinOrUid)
member = members!.get(memberUinOrUidStr)
}
return member
}
let member = getMember()
if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupQQ)
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember()
}
return member
@@ -102,7 +80,7 @@ const selfInfo: SelfInfo = {
}
export async function getSelfNick(force = false): Promise<string> {
if (!selfInfo.nick || force) {
if ((!selfInfo.nick || force) && selfInfo.uid) {
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
selfInfo.nick = userInfo.nick
@@ -127,4 +105,24 @@ export function getSelfUid() {
export function getSelfUin() {
return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
const 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,253 +0,0 @@
import { Level } from 'level'
import { type GroupNotify, RawMessage } from '../ntqqapi/types'
import { DATA_DIR } from './utils'
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() {
}
init(uin: string) {
const DB_PATH = DATA_DIR + `/msg_${uin}`
this.db = new Level(DB_PATH, { valueEncoding: 'json' })
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

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

View File

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

View File

@@ -30,6 +30,8 @@ export interface Config {
ffmpeg?: string // ffmpeg路径
musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */
msgCacheExpire?: number
}
export interface LLOneBotError {
@@ -41,11 +43,22 @@ export interface LLOneBotError {
export interface FileCache {
fileName: string
filePath: string
fileSize: string
fileUuid?: string
url?: string
msgId?: string
msgId: string
peerUid: string
chatType: number
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,11 +16,13 @@ export interface ListenerIBase {
new(listener: any): ListenerClassBase
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20
export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
public initialised = false
constructor() {
}
@@ -33,7 +35,7 @@ export class NTEventWrapper {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
current.DispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
}
}
// 如果方法存在,正常返回
@@ -45,9 +47,10 @@ export class NTEventWrapper {
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession
this.initialised = true
}
CreatEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/')
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
@@ -68,14 +71,14 @@ export class NTEventWrapper {
}
}
CreatListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
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)
//console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
@@ -84,7 +87,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)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout)
@@ -100,7 +103,7 @@ export class NTEventWrapper {
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) => {
const EventFunc = this.CreatEventFunction<EventType>(EventName)
const EventFunc = this.createEventFunction<EventType>(EventName)
let complete = false
const Timeouter = setTimeout(() => {
if (!complete) {
@@ -149,7 +152,7 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName)
this.createListenerFunction(ListenerMainName)
})
}
@@ -195,8 +198,8 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.CreatListenerFunction(ListenerMainName)
const EventFunc = this.CreatEventFunction<EventType>(EventName)
this.createListenerFunction(ListenerMainName)
const EventFunc = this.createEventFunction<EventType>(EventName)
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,10 +1,9 @@
import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import path from 'node:path'
import { log, TEMP_DIR } from './index'
import { dbUtil } from '../db'
import * as fileType from 'file-type'
import { TEMP_DIR } from './index'
import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
export function isGIF(path: string) {
const buffer = Buffer.alloc(4)
@@ -33,31 +32,6 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
})
}
export async function file2base64(path: string) {
let result = {
err: '',
data: '',
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000)
} catch (e: any) {
result.err = e.toString()
return result
}
const data = await fsPromise.readFile(path)
// 转换为Base64编码
result.data = data.toString('base64')
} catch (err: any) {
result.err = err.toString()
}
return result
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
@@ -110,119 +84,118 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
return Buffer.from(await fetchRes.arrayBuffer())
}
export enum FileUriType {
Unknown = 0,
FileURL = 1,
RemoteURL = 2,
OneBotBase64 = 3,
DataURL = 4,
Path = 5
}
export function checkUriType(uri: string): { type: FileUriType } {
if (uri.startsWith('base64://')) {
return { type: FileUriType.OneBotBase64 }
}
if (uri.startsWith('data:')) {
return { type: FileUriType.DataURL }
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
return { type: FileUriType.RemoteURL }
}
if (uri.startsWith('file://')) {
return { type: FileUriType.FileURL }
}
try {
if (fs.existsSync(uri)) return { type: FileUriType.Path }
} catch { }
return { type: FileUriType.Unknown }
}
interface FetchFileRes {
data: Buffer
url: string
}
async function fetchFile(url: string): Promise<FetchFileRes> {
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
'Host': new URL(url).hostname
}
const raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`)
return {
data: Buffer.from(await raw.arrayBuffer()),
url: raw.url
}
}
type Uri2LocalRes = {
success: boolean
errMsg: string
fileName: string
ext: string
path: string
isLocal: boolean
}
export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> {
let res = {
success: false,
errMsg: '',
fileName: '',
ext: '',
path: '',
isLocal: false,
}
if (!fileName) {
fileName = randomUUID()
}
let filePath = path.join(TEMP_DIR, fileName)
let url: URL | null = null
try {
url = new URL(uri)
} catch (e: any) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
export async function uri2local(uri: string, filename?: string): Promise<Uri2LocalRes> {
const { type } = checkUriType(uri)
if (type === FileUriType.FileURL) {
const filePath = fileURLToPath(uri)
const fileName = path.basename(filePath)
return { success: true, errMsg: '', fileName, path: filePath, isLocal: true }
}
// log("uri protocol", url.protocol, uri);
if (url.protocol == 'base64:') {
// base64转成文件
let base64Data = uri.split('base64://')[1]
try {
const buffer = Buffer.from(base64Data, 'base64')
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件
let buffer: Buffer | null = null
try {
buffer = await httpDownload(uri)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name) {
fileName = pathInfo.name
if (pathInfo.ext) {
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_')
res.fileName = fileName
filePath = path.join(TEMP_DIR, randomUUID() + fileName)
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string
if (url.protocol === 'file:') {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === 'win32') {
filePath = pathname.slice(1)
} else {
filePath = pathname
}
} else {
const cache = await dbUtil.getFileCache(uri)
if (cache) {
filePath = cache.filePath
} else {
filePath = uri
}
}
if (type === FileUriType.Path) {
const fileName = path.basename(uri)
return { success: true, errMsg: '', fileName, path: uri, isLocal: true }
}
res.isLocal = true
}
// else{
// res.errMsg = `不支持的file协议,` + url.protocol
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
if (type === FileUriType.RemoteURL) {
try {
const ext = (await fileType.fileTypeFromFile(filePath))?.ext
if (ext) {
log('获取文件类型', ext, filePath)
await fsPromise.rename(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
const res = await fetchFile(uri)
const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
if (match?.[1]) {
filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_')
} else {
filename ??= randomUUID()
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
const filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data)
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e: any) {
const errMsg = `${uri}下载失败,` + e.toString()
return { success: false, errMsg, fileName: '', path: '', isLocal: false }
}
}
res.success = true
res.path = filePath
return res
if (type === FileUriType.OneBotBase64) {
filename ??= randomUUID()
const filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64')
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
}
if (type === FileUriType.DataURL) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri)
if (capture) {
filename ??= randomUUID()
const [, _type, base64] = capture
const filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64')
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
}
}
return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false }
}
export async function copyFolder(sourcePath: string, destPath: string) {

View File

@@ -143,4 +143,27 @@ export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey:
}
}
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 fs from 'fs'
export * from './file'
export * from './helper'
@@ -7,12 +6,9 @@ export * from './log'
export * from './qqlevel'
export * from './QQBasicInfo'
export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR = global.LiteLoader.plugins['LLOneBot'].path.plugin
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR: string = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin
export { getVideoInfo } from './video'
export { checkFfmpeg } from './video'
export { encodeSilk } from './audio'

View File

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

View File

@@ -1,3 +1,4 @@
// 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()

View File

@@ -1,6 +1,7 @@
// 运行在 Electron 主进程 下的插件入口
import { BrowserWindow, dialog, ipcMain } from 'electron'
import path from 'node:path'
import fs from 'node:fs'
import { Config } from '../common/types'
import {
@@ -13,20 +14,20 @@ import {
CHANNEL_UPDATE,
} from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR } from '../common/utils'
import { DATA_DIR, getBuildVersion, TEMP_DIR } from '../common/utils'
import {
getGroupMember,
llonebotError,
setSelfInfo,
getSelfInfo,
getSelfUid,
getSelfUin
getSelfUin,
addMsgCache
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import {
FriendRequestNotify,
GroupNotifies,
GroupNotify,
GroupNotifyTypes,
RawMessage,
BuddyReqType,
@@ -36,8 +37,7 @@ import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import path from 'node:path'
import { dbUtil } from '../common/db'
import { MessageUnique } from '../common/utils/MessageUnique'
import { setConfig } from './setConfig'
import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
@@ -48,6 +48,7 @@ import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/
import '../ntqqapi/wrapper'
import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperConstructor, getSession } from '../ntqqapi/wrapper'
import { Peer } from '../ntqqapi/types'
let mainWindow: BrowserWindow | null = null
@@ -94,7 +95,7 @@ function onLoad() {
}
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
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 error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace('\n\n', '\n')
@@ -148,14 +149,16 @@ function onLoad() {
const { debug, reportSelfMessage } = getConfigUtil().getConfig()
for (let message of msgList) {
// 过滤启动之前的消息
// log('收到新消息', message);
if (parseInt(message.msgTime) < startTime / 1000) {
continue
}
// log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message)
// }
const peer: Peer = {
chatType: message.chatType,
peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
addMsgCache(message)
OB11Constructor.message(message)
.then((msg) => {
@@ -180,19 +183,12 @@ function onLoad() {
}
})
OB11Constructor.PrivateEvent(message).then((privateEvent) => {
log(message)
//log(message)
if (privateEvent) {
// log("post private event", privateEvent);
postOb11Event(privateEvent)
}
})
// OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
// log(message)
// if (friendAddEvent) {
// // log("post friend add event", friendAddEvent);
// postOb11Event(friendAddEvent)
// }
// })
}
}
@@ -210,29 +206,22 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
log('message update', message.msgId, message)
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
continue
}
recallMsgIds.push(message.msgId)
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId)
if (!oriMessageId) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then()
message.msgShortId = oriMessage.msgShortId
OB11Constructor.RecallEvent(message).then((recallEvent) => {
OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => {
if (recallEvent) {
log('post recall event', recallEvent)
//log('post recall event', recallEvent)
postOb11Event(recallEvent)
}
})
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
dbUtil.updateMsg(message).then()
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
@@ -247,6 +236,7 @@ function onLoad() {
log('report self message error: ', e.stack.toString())
}
})
const processedGroupNotify: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
@@ -254,54 +244,43 @@ function onLoad() {
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notify: GroupNotifies
let notifies: GroupNotify[]
try {
notify = await NTQQGroupApi.getGroupNotifies()
notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) {
// log("获取群通知详情失败", e);
return
}
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) {
try {
notify.time = Date.now()
// const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
// if (notifyTime < startTime) {
// continue;
// }
let existNotify = await dbUtil.getGroupNotify(notify.seq)
if (existNotify) {
const notifyTime = parseInt(notify.seq) / 1000
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notifyTime < startTime || processedGroupNotify.includes(flag)) {
continue
}
log('收到群通知', notify)
await dbUtil.addGroupNotify(notify)
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
processedGroupNotify.push(flag)
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify)
try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid)
let operatorId = member1.uin
let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid)
operatorId = member2?.uin!
subType = 'kick'
const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))!
let operatorId = member1Uin
let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) {
// 是被踢的
const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid)
if (member2Uin) {
operatorId = member2Uin
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1.uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
} catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
subType = 'kick'
}
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)) {
log('有加群请求')
@@ -367,10 +346,13 @@ function onLoad() {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
continue
}
if (+req.reqTime < startTime / 1000) {
continue
}
let userId = 0
try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin!)
userId = parseInt(requesterUin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
@@ -389,20 +371,28 @@ function onLoad() {
let startTime = 0 // 毫秒
async function start(uid: string, uin: string) {
log('llonebot pid', process.pid)
log('process pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
llonebotError.otherError = ''
startTime = Date.now()
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! })
dbUtil.init(uin)
const WrapperSession = getSession()
if (WrapperSession) {
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession })
}
MessageUnique.init(uin)
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log)
//log('start activate group member info')
// 下面两个会导致CPU占用过高QQ卡死
// NTQQGroupApi.activateMemberInfoChange().then().catch(log)
// NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
if (config.ob11.enableHttp) {
@@ -421,7 +411,9 @@ function onLoad() {
log('LLOneBot start')
}
const init = async () => {
const buildVersion = getBuildVersion()
const intervalId = setInterval(() => {
const current = getSelfInfo()
if (!current.uin) {
setSelfInfo({
@@ -430,20 +422,16 @@ function onLoad() {
nick: current.uin,
})
}
//log('self info', selfInfo, globalThis.authData)
if (current.uin) {
if (current.uin && (buildVersion >= 27187 || getSession())) {
clearInterval(intervalId)
start(current.uid, current.uin)
}
else {
setTimeout(init, 1000)
}
}
init()
}, 600)
}
// 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) {
if (getSelfUid()) {
if (window.id !== 2) {
return
}
mainWindow = window

View File

@@ -1,4 +1,5 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import {
CacheFileList,
CacheFileListItem,
@@ -16,7 +17,7 @@ import path from 'node:path'
import fs from 'node:fs'
import { ReceiveCmdS } from '../hook'
import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { rkeyManager } from '@/ntqqapi/helper/rkey'
import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file'
@@ -24,42 +25,23 @@ import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { Time } from 'cosmokit'
export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
/** 27187 TODO */
static async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession()
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 }))?.urlResult?.domainUrl[0]?.url!
{ downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url
}
static async getFileType(filePath: string) {
return fileTypeFromFile(filePath)
}
static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY,
args: [
{
fromPath: filePath,
toPath: destPath,
},
],
})
}
static async getFileSize(filePath: string) {
return await callNTQQApi<number>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_SIZE,
args: [filePath],
})
}
// 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath)
@@ -72,22 +54,43 @@ export class NTQQFileApi {
fileName += ext
}
const session = getSession()
const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
})
await fsPromise.copyFile(filePath, mediaPath!)
let mediaPath: string
if (session) {
mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
})
} else {
mediaPath = await invoke<string>({
methodName: NTMethod.MEDIA_FILE_PATH,
args: [
{
path_info: {
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
},
],
})
}
await fsPromise.copyFile(filePath, mediaPath)
const fileSize = (await fsPromise.stat(filePath)).size
return {
md5: fileMd5,
fileName,
path: mediaPath!,
path: mediaPath,
fileSize,
ext
}
@@ -108,53 +111,79 @@ export class NTQQFileApi {
if (force) {
try {
await fsPromise.unlink(sourcePath)
} catch (e) {
//
}
} catch { }
} else {
return sourcePath
}
}
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
let filePath: string
if (NTEventDispatch.initialised) {
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
},
{
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
}
return false
},
{
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
}
)
let filePath = data[1].filePath
)
filePath = data[1].filePath
} else {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>({
methodName: NTMethod.DOWNLOAD_MEDIA,
args: [
{
getReq: {
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
],
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.msgId === msgId,
timeout
})
filePath = data.notifyInfo.filePath
}
if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath)
@@ -164,9 +193,9 @@ export class NTQQFileApi {
}
static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number; height: number }>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.IMAGE_SIZE,
return await invoke<{ width: number; height: number }>({
className: NTClass.FS_API,
methodName: NTMethod.IMAGE_SIZE,
args: [filePath],
})
}
@@ -178,7 +207,6 @@ export class NTQQFileApi {
const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr
const fileUuid = element.fileUuid
if (url) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
@@ -207,8 +235,8 @@ export class NTQQFileApi {
export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
return await invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_SET_SILENCE,
args: [
{
isSilent,
@@ -219,21 +247,21 @@ export class NTQQFileCacheApi {
}
static getCacheSessionPathList() {
return callNTQQApi<
return invoke<
{
key: string
value: string
}[]
>({
className: NTQQApiClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION,
className: NTClass.OS_API,
methodName: NTMethod.CACHE_PATH_SESSION,
})
}
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({
return invoke<any>({
// TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
methodName: NTMethod.CACHE_CLEAR,
args: [
{
keys: cacheKeys,
@@ -244,8 +272,8 @@ export class NTQQFileCacheApi {
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
return invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_ADD_SCANNED_PATH,
args: [
{
pathMap: { ...pathMap },
@@ -256,35 +284,35 @@ export class NTQQFileCacheApi {
}
static scanCache() {
callNTQQApi<GeneralCallResult>({
invoke<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then()
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
return invoke<CacheScanResult>({
methodName: NTMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
timeout: 300 * Time.second,
})
}
static getHotUpdateCachePath() {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE,
return invoke<string>({
className: NTClass.HOTUPDATE_API,
methodName: NTMethod.CACHE_PATH_HOT_UPDATE,
})
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP,
return invoke<string>({
className: NTClass.BUSINESS_API,
methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP,
})
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET,
invoke<ChatCacheList>({
methodName: NTMethod.CACHE_CHAT_GET,
args: [
{
chatType: type,
@@ -303,8 +331,8 @@ export class NTQQFileCacheApi {
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : { fileType: fileType }
return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET,
return invoke<CacheFileList>({
methodName: NTMethod.CACHE_FILE_GET,
args: [
{
fileType: fileType,
@@ -319,8 +347,8 @@ export class NTQQFileCacheApi {
}
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
return await invoke<GeneralCallResult>({
methodName: NTMethod.CACHE_CHAT_CLEAR,
args: [
{
chats,

View File

@@ -1,15 +1,15 @@
import { Friend, FriendV2 } from '../types'
import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { CacheClassFuncAsyncExtend } from '@/common/utils/helper'
import { LimitedHashTable } from '@/common/utils/table'
export class NTQQFriendApi {
/** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) {
const data = await callNTQQApi<{
const data = await invoke<{
data: {
categoryId: number
categroyName: string
@@ -17,12 +17,11 @@ export class NTQQFriendApi {
buddyList: Friend[]
}[]
}>({
methodName: NTQQApiMethod.FRIENDS,
methodName: NTMethod.FRIENDS,
args: [{ force_update: forced }, undefined],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
// log('获取好友列表', data)
let _friends: Friend[] = []
for (const fData of data.data) {
_friends.push(...fData.buddyList)
@@ -30,23 +29,6 @@ export class NTQQFriendApi {
return _friends
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0,
},
},
null,
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) {
const data = flag.split('|')
if (data.length < 2) {
@@ -55,71 +37,143 @@ export class NTQQFriendApi {
const friendUid = data[0]
const reqTime = data[1]
const session = getSession()
return session?.getBuddyService().approvalFriendRequest({
friendUid,
reqTime,
accept
})
if (session) {
return session.getBuddyService().approvalFriendRequest({
friendUid,
reqTime,
accept
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_FRIEND_REQUEST,
args: [
{
approvalInfo: {
friendUid,
reqTime,
accept,
},
},
],
})
}
}
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = []
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
)
return Array.from(data.values())
}
@CacheClassFuncAsyncExtend(3600 * 1000, 'getBuddyIdMap', () => true)
static async getBuddyIdMapCache(refresh = false): Promise<LimitedHashTable<string, string>> {
return await NTQQFriendApi.getBuddyIdMap(refresh)
if (session) {
const uids: string[] = []
const buddyService = session.getBuddyService()
const buddyListV2 = 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
)
return Array.from(data.values())
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Map<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
return Array.from(data.userSimpleInfos.values())
}
}
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)
if (session) {
const uids: string[] = []
const buddyService = session?.getBuddyService()
const buddyListV2 = 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!)
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Map<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
data.userSimpleInfos.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
}
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
})
if (session) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const buddyService = session.getBuddyService()
const buddyListV2 = (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
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Map<string, SimpleInfo>
}>({
className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
})
return Array.from(data.userSimpleInfos).map(([key, value]) => {
if (value.baseInfo) {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: data.buddyCategory.find(e => e.categoryId === value.baseInfo.categoryId)?.categroyName
}
}
return value
})
}
}
static async isBuddy(uid: string): Promise<boolean> {
const session = getSession()
return session?.getBuddyService().isBuddy(uid)!
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke<boolean>({
methodName: 'nodeIKernelBuddyService/isBuddy',
args: [
{ uid },
null,
],
})
}
}
}

View File

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

View File

@@ -1,36 +1,25 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { getSelfNick, getSelfUid } from '../../common/data'
import { getBuildVersion } from '../../common/utils'
import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask'
function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000)
const random = Math.floor(Math.random() * Math.pow(2, 32))
const buffer = Buffer.alloc(8)
buffer.writeUInt32BE(timestamp, 0)
buffer.writeUInt32BE(random, 4)
const msgId = BigInt('0x' + buffer.toString('hex')).toString()
return msgId
}
export class NTQQMsgApi {
/** 27187 TODO */
static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession()
return session?.getMsgService().getTempChatInfo(chatType, peerUid)!
}
static async prepareTempChat(toUserUid: string, GroupCode: string, nickname: string) {
//By Jadx/Ida Mlikiowa
let TempGameSession = {
nickname: '',
gameAppId: '',
selfTinyId: '',
peerRoleId: '',
peerOpenId: '',
}
const session = getSession()
return session?.getMsgService().prepareTempChat({
chatType: ChatType2.KCHATTYPETEMPC2CFROMGROUP,
peerUid: toUserUid,
peerNickname: nickname,
fromGroupCode: GroupCode,
sig: '',
selfPhone: '',
selfUid: getSelfUid(),
gameSession: TempGameSession
})
return session?.getMsgService().getTempChatInfo(chatType, peerUid)
}
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
@@ -39,28 +28,54 @@ export class NTQQMsgApi {
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString()
const session = getSession()
return session?.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
if (session) {
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
} else {
return await invoke({
methodName: NTMethod.EMOJI_LIKE,
args: [
{
peer,
msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
}
}
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
const session = getSession()
return session?.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)!
if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.GET_MULTI_MSG,
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
}
}
static async activateChat(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
return await invoke<GeneralCallResult>({
methodName: NTMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null],
})
}
static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY,
return await invoke<GeneralCallResult>({
methodName: NTMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样
args: [{ peer, cnt: 20 }, null],
})
@@ -70,68 +85,120 @@ export class NTQQMsgApi {
if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession()
//Mlikiowa 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得
return await session?.getMsgService().getMsgsByMsgId(peer, msgIds)!
if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsByMsgId',
args: [
{
peer,
msgIds,
},
null,
],
})
}
}
static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
const session = getSession()
// 消息时间从旧到新
return session?.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)!
if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.HISTORY_MSG,
args: [
{
peer,
msgId,
cnt: count,
queryOrder: isReverseOrder,
},
null,
],
})
}
}
static async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession()
return await session?.getMsgService().recallMsg({
chatType: peer.chatType,
peerUid: peer.peerUid
}, msgIds)
if (session) {
return session.getMsgService().recallMsg({
chatType: peer.chatType,
peerUid: peer.peerUid
}, msgIds)
} else {
return await invoke({
methodName: NTMethod.RECALL_MSG,
args: [
{
peer,
msgIds,
},
null,
],
})
}
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
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()
}
const 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
let msgList: RawMessage[]
if (NTEventDispatch.initialised) {
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 (const 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 => {
return false
},
'0',
peer,
msgElements,
new Map()
)
msgList = data[1]
} else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/sendMsg',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
args: [
{
msgId: '0',
peer,
msgElements,
msgAttributeInfos: new Map()
},
null
],
timeout
})
msgList = data.msgList
}
const retMsg = msgList.find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
}
@@ -139,102 +206,85 @@ export class NTQQMsgApi {
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[]) {
const session = getSession()
return session?.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])!
if (session) {
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} else {
return await invoke<GeneralCallResult>({
methodName: NTMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
}
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const msgInfos = msgIds.map(async id => {
return { msgId: id, senderShowName: await getSelfNick() }
const senderShowName = await getSelfNick()
const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
})
const selfUid = getSelfUid()
let data = await NTEventDispatch.CallNormalEvent<
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/multiForwardMsgWithComment',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
let msgList: RawMessage[]
if (NTEventDispatch.initialised) {
const data = await NTEventDispatch.CallNormalEvent<
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/multiForwardMsgWithComment',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
}
return false
},
msgInfos,
srcPeer,
destPeer,
[],
new Map()
)
for (let msg of data[1]) {
return false
},
msgInfos,
srcPeer,
destPeer,
[],
new Map()
)
msgList = data[1]
} else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/multiForwardMsgWithComment',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
},
args: [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
msgList = data.msgList
}
for (const msg of msgList) {
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
continue
@@ -250,23 +300,61 @@ export class NTQQMsgApi {
throw new Error('转发消息超时')
}
static async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsBySeqAndCount',
args: [
{
peer,
cnt: count,
msgSeq: seq,
queryOrder: desc
},
null,
],
})
}
}
/** 27187 TODO */
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isIncludeCurrent: true,
pageLimit: 1,
pageLimit: count,
})
return ret!
return ret
}
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
static async getSingleMsg(peer: Peer, seq: string) {
const session = getSession()
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)!
if (session) {
return await session.getMsgService().getSingleMsg(peer, seq)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getSingleMsg',
args: [
{
peer,
msgSeq: seq,
},
null,
],
})
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,30 @@
import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTClass, NTMethod } from './ntcall'
import { NTQQMsgApi } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types'
import {
deleteGroup,
CategoryFriend,
ChatType,
GroupMember,
GroupMemberRole,
RawMessage,
SimpleInfo, User,
} from './types'
import {
friends,
getFriend,
getGroupMember,
groups,
getSelfUin,
setSelfInfo
} from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config'
import fs from 'fs'
import { dbUtil } from '@/common/db'
import { NTQQGroupApi } from './api/group'
import { getConfigUtil } from '@/common/config'
import fs from 'node:fs'
import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique'
import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { randomUUID } from 'node:crypto'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -47,83 +49,74 @@ export let ReceiveCmdS = {
CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate',
}
} as const
export type ReceiveCmd = (typeof ReceiveCmdS)[keyof typeof ReceiveCmdS]
export type ReceiveCmd = string
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
interface NTQQApiReturnData<Payload = unknown> extends Array<any> {
0: {
type: 'request'
eventName: NTQQApiClass
eventName: NTClass
callbackId?: string
}
1: {
cmdName: ReceiveCmd
cmdType: 'event'
payload: PayloadType
payload: Payload
}[]
}
let receiveHooks: Array<{
const logHook = false
const receiveHooks: Array<{
method: ReceiveCmd[]
hookFunc: (payload: any) => void | Promise<void>
id: string
}> = []
let callHooks: Array<{
method: NTQQApiMethod[]
const callHooks: Array<{
method: NTMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = []
export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send
const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
// console.log("hookNTQQApiReceive", channel, args)
let isLogger = false
try {
isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) { }
if (!isLogger) {
try {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
} catch (e) {
log('hook log error', e, args)
const isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
if (logHook && !isLogger) {
log(`received ntqq api message: ${channel}`, args)
}
}
try {
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (let hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
; (_ as Promise<void>).then()
}
} catch (e) {
log('hook error', e, receiveData.payload)
}
}).then()
}
} catch { }
if (args?.[1] instanceof Array) {
for (const receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (const hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) {
new Promise((resolve, reject) => {
try {
hook.hookFunc(receiveData.payload)
} catch (e: any) {
log('hook error', ntQQApiMethodName, e.stack.toString())
}
resolve(undefined)
}).then()
}
}
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1])
}).then()
delete hookApiCallbacks[callbackId]
}
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1])
resolve(undefined)
}).then()
delete hookApiCallbacks[callbackId]
}
} catch (e: any) {
log('hookNTQQApiReceive error', e.stack.toString(), args)
}
originalSend.call(window.webContents, channel, ...args)
}
@@ -144,23 +137,21 @@ export function hookNTQQApiCall(window: BrowserWindow) {
} catch (e) { }
if (!isLogger) {
try {
HOOK_LOG && log('call NTQQ api', thisArg, args)
logHook && log('call NTQQ api', thisArg, args)
} catch (e) { }
try {
const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod
const cmdName: NTMethod = _args[0] as NTMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
(_ as Promise<void>).then()
}
} catch (e) {
hook.hookFunc(callParams)
} catch (e: any) {
log('hook call error', e, _args)
}
resolve(undefined)
}).then()
}
})
@@ -179,16 +170,16 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
// console.log(args);
HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
//HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs)
},
})
let ret = target.apply(thisArg, args)
try {
/*try {
HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) { }
} catch (e) { }*/
return ret
},
})
@@ -216,7 +207,7 @@ export function registerReceiveHook<PayloadType>(
}
export function registerCallHook(
method: NTQQApiMethod | NTQQApiMethod[],
method: NTMethod | NTMethod[],
hookFunc: (callParams: unknown[]) => void | Promise<void>,
): void {
if (!Array.isArray(method)) {
@@ -233,16 +224,16 @@ export function removeReceiveHook(id: string) {
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) {
log('update group', group)
log('update group', group.groupCode)
if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode)
continue
}
log('update group', group)
//log('update group', group)
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().catch(log)
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
@@ -260,9 +251,9 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
}
}
}
}
}*/
async function processGroupEvent(payload: { groupList: Group[] }) {
/*async function processGroupEvent(payload: { groupList: Group[] }) {
try {
const newGroupList = payload.groupList
for (const group of newGroupList) {
@@ -313,12 +304,12 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
}
}*/
export async function startHook() {
// 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
/*registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
@@ -332,7 +323,7 @@ export async function startHook() {
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
// log("群列表变动, store", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
@@ -341,7 +332,7 @@ export async function startHook() {
processGroupEvent(payload).then()
}
}
})
})*/
registerReceiveHook<{
groupCode: string
@@ -391,17 +382,32 @@ export async function startHook() {
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
for (const fData of payload.data) {
const _friends = fData.buddyList
for (let friend of _friends) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
// log("onBuddyListChange", payload)
// let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
let friendList: User[] = [];
if ((payload as any).userSimpleInfos) {
// friendListV2 = payload as any
friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
return {
...v.coreInfo,
}
})
}
else {
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
}
}
log('好友列表变动', friendList.length)
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)
}
}
})
@@ -444,8 +450,12 @@ export async function startHook() {
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord
dbUtil.addMsg(message).then()
const { msgId, chatType, peerUid } = msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
@@ -489,7 +499,7 @@ export async function startHook() {
}
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
@@ -508,4 +518,4 @@ export async function startHook() {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
}
}

View File

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

View File

@@ -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

@@ -1,3 +1,4 @@
export * from './common'
export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService'
export * from './NodeIKernelGroupService'
@@ -7,4 +8,5 @@ export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService'
export * from './NodeIKernelTipOffService'
export * from './NodeIKernelSearchService'

View File

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

View File

@@ -78,8 +78,10 @@ export interface Friend extends User {
export interface CategoryFriend {
categoryId: number
categorySortId: number
categroyName: string
categroyMbCount: number
onlineCount: number
buddyList: User[]
}
@@ -340,4 +342,4 @@ export interface UserDetailInfoByUin {
pendantId: string
vipNameColorId: string
}
}
}

View File

@@ -8,7 +8,8 @@ import {
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService
NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services'
import os from 'node:os'
const Process = require('node:process')
@@ -25,6 +26,7 @@ export interface NodeIQQNTWrapperSession {
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
getSearchService(): NodeIKernelSearchService
}
export interface WrapperApi {

View File

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

View File

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

View File

@@ -3,4 +3,11 @@ import { ActionName } from '../types'
export default class GetImage extends GetFileBase {
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 { ActionName } from '../types';
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { dbUtil } from '@/common/db';
import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload {
message_id: number | string;
message_id: number | string
}
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg;
protected async _handle(payload: Payload): Promise<any> {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString()));
if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found');
throw new Error('msg not found')
}
return await NTQQGroupApi.removeGroupEssence(
msg.peerUid,
msg.msgId
);
msg.Peer.peerUid,
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 { ActionName } from '../types'
import fs from 'fs'
import { join as joinPath } from 'node:path'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '../../../common/utils'
import fsPromise from 'fs/promises'
import path from 'node:path'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils'
import { randomUUID } from 'node:crypto'
interface Payload {
@@ -22,15 +23,15 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name
let name = payload.name || randomUUID()
const filePath = joinPath(TEMP_DIR, name)
const name = payload.name ? path.basename(payload.name) : randomUUID()
const filePath = path.join(TEMP_DIR, name)
if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64')
await fsPromise.writeFile(filePath, payload.base64, 'base64')
} else if (payload.url) {
const headers = this.getHeaders(payload.headers)
let buffer = await httpDownload({ url: payload.url, headers: headers })
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary')
const buffer = await httpDownload({ url: payload.url, headers: headers })
await fsPromise.writeFile(filePath, buffer)
} else {
throw new Error('不存在任何文件, 无法下载')
}
@@ -38,8 +39,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath)
const newPath = joinPath(TEMP_DIR, md5)
fs.renameSync(filePath, newPath)
const newPath = path.join(TEMP_DIR, md5)
await fsPromise.rename(filePath, newPath)
return { file: newPath }
}
return { file: filePath }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,58 @@
import { OB11GroupMember } from '../../types'
import { getGroup } from '../../../common/data'
import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { log } from '../../../common/utils'
import { NTQQGroupApi, WebApi } from '@/ntqqapi/api'
import { getSelfUid } from '@/common/data'
export interface PayloadType {
group_id: number
interface Payload {
group_id: number | string
no_cache: boolean | string
}
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> {
class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList
protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString())
if (group) {
if (!group.members?.length || payload.no_cache === true || payload.no_cache === 'true') {
const members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
group.members = Array.from(members.values())
log('强制刷新群成员列表, 数量: ', group.members.length)
}
return OB11Constructor.groupMembers(group)
} else {
throw `${payload.group_id}不存在`
protected async _handle(payload: Payload) {
const groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
const groupMembersArr = Array.from(groupMembers.values())
let _groupMembers = groupMembersArr.map(item => {
return OB11Constructor.groupMember(payload.group_id.toString(), item)
})
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
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

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

View File

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

View File

@@ -1,27 +1,24 @@
import { ActionName } from '../types'
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 {
message_id: number
message_id: number | string
}
class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg
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) {
throw `消息${payload.message_id}不存在`
}
await NTQQMsgApi.recallMsg(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
[msg.msgId],
)
await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId])
}
}

View File

@@ -1,12 +1,12 @@
import BaseAction from '../BaseAction'
import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType } from '@/ntqqapi/types'
import { dbUtil } from '@/common/db'
import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload {
message_id: number
message_id: number | string
group_id: number | string
user_id?: number | string
}
@@ -24,19 +24,15 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
}
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 ret = await NTQQMsgApi.forwardMsg(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
peer,
[msg.msgId],
)
const ret = await NTQQMsgApi.forwardMsg(msg.Peer, peer, [msg.MsgId])
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`)
}

View File

@@ -2,10 +2,12 @@ import { OB11Message } from '../../types'
import { OB11Constructor } from '../../constructor'
import BaseAction from '../BaseAction'
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 {
message_id: number
message_id: number | string
}
export type ReturnDataType = OB11Message
@@ -18,14 +20,22 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
if (!payload.message_id) {
throw '参数message_id不能为空'
}
let msg = await dbUtil.getMsgByShortId(payload.message_id)
if (!msg) {
msg = await dbUtil.getMsgByLongId(payload.message_id.toString())
const msgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString())
const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(msgShortId || +payload.message_id)
if (!msgIdWithPeer) {
throw ('消息不存在')
}
if (!msg) {
throw '消息不存在'
const peer = {
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

@@ -3,35 +3,35 @@ import {
ChatType,
ElementType,
GroupMemberRole,
PicSubType,
RawMessage,
SendMessageElement,
} from '../../../ntqqapi/types'
import { getGroup, getGroupMember, getSelfUid, getSelfUin } from '../../../common/data'
} from '@/ntqqapi/types'
import { getGroupMember, getSelfUid, getSelfUin } from '@/common/data'
import {
OB11MessageCustomMusic,
OB11MessageData,
OB11MessageDataType,
OB11MessageFile,
OB11MessageJson,
OB11MessageMixType,
OB11MessageMusic,
OB11MessageNode,
OB11PostSendMsg,
} from '../../types'
import { SendMsgElementConstructor } from '../../../ntqqapi/constructor'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import BaseAction from '../BaseAction'
import { ActionName, BaseCheckResult } from '../types'
import fs from 'node:fs'
import fsPromise from 'node:fs/promises'
import { decodeCQCode } from '../../cqcode'
import { dbUtil } from '../../../common/db'
import { getConfigUtil } from '../../../common/config'
import { log } from '../../../common/utils/log'
import { sleep } from '../../../common/utils/helper'
import { uri2local } from '../../../common/utils'
import { getConfigUtil } from '@/common/config'
import { log } from '@/common/utils/log'
import { sleep } from '@/common/utils/helper'
import { uri2local } from '@/common/utils'
import { NTQQGroupApi, NTQQMsgApi, NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { Peer } from '@/ntqqapi/types/msg'
import { MessageUnique } from '@/common/utils/MessageUnique'
import { OB11MessageFileBase } from '../../types'
export interface ReturnDataType {
message_id: number
@@ -43,6 +43,11 @@ export enum ContextMode {
Group = 2
}
interface MessageContext {
deleteAfterSentFiles: string[]
peer: Peer
}
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === 'string') {
if (autoEscape === true) {
@@ -65,6 +70,32 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa
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(
messageData: OB11MessageData[],
peer: Peer,
@@ -88,9 +119,8 @@ export async function createSendElements(
if (!peer) {
continue
}
let atQQ = sendMsg.data?.qq
if (atQQ) {
atQQ = atQQ.toString()
if (sendMsg.data?.qq) {
const atQQ = String(sendMsg.data.qq)
if (atQQ === 'all') {
// todo查询剩余的at全体次数
const groupCode = peer.peerUid
@@ -130,9 +160,16 @@ export async function createSendElements(
}
break
case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id
if (replyMsgId) {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId))
if (sendMsg.data?.id) {
const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
if (!replyMsgId) {
log('回复消息不存在', replyMsgId)
continue
}
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId(
replyMsgId.Peer,
[replyMsgId.MsgId!]
)).msgList[0]
if (replyMsg) {
sendElements.push(
SendMsgElementConstructor.reply(
@@ -164,66 +201,36 @@ export async function createSendElements(
)
}
break
case OB11MessageDataType.image:
case OB11MessageDataType.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice: {
const data = (sendMsg as OB11MessageFile).data
let file = data.file
const payloadFileName = data?.name
if (file) {
const cache = await dbUtil.getFileCache(file)
if (cache) {
if (fs.existsSync(cache.filePath)) {
file = 'file://' + cache.filePath
}
else if (cache.downloadFunc) {
await cache.downloadFunc()
file = cache.filePath
}
else if (cache.url) {
file = cache.url
}
log('找到文件缓存', file)
}
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,
),
)
}
}
case OB11MessageDataType.image: {
const res = await SendMsgElementConstructor.pic(
(await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '',
sendMsg.data.subType || 0
)
deleteAfterSentFiles.push(res.picElement.sourcePath)
sendElements.push(res)
}
break
case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendMsgElementConstructor.file(path, fileName))
}
break
case OB11MessageDataType.video: {
const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, { deleteAfterSentFiles })
let thumb = sendMsg.data.thumb
if (thumb) {
const uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path
}
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
case OB11MessageDataType.json: {
@@ -282,14 +289,13 @@ export async function sendMsg(
log('文件大小计算失败', e, fileElement)
}
}
log('发送消息总大小', totalSize, 'bytes')
let timeout = ((totalSize / 1024 / 100) * 1000) + 5000 // 100kb/s
log('设置消息超时时间', timeout)
//log('发送消息总大小', totalSize, 'bytes')
const timeout = 10000 + (totalSize / 1024 / 256 * 1000) // 10s Basic Timeout + PredictTime( For File 512kb/s )
//log('设置消息超时时间', timeout)
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout)
log('消息发送结果', returnMsg)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
log('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg
}
@@ -299,10 +305,9 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
// This redundant design of Ob11 here should be blamed.
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
const group = (await getGroup(payload.group_id))! // checked before
return {
chatType: ChatType.group,
peerUid: group.groupCode
peerUid: payload.group_id.toString(),
}
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
@@ -312,7 +317,7 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
return {
chatType: isBuddy ? ChatType.friend : ChatType.temp,
peerUid: Uid!,
guildId: payload.group_id || ''//临时主动发起时需要传入群号
guildId: payload.group_id?.toString() || '' //临时主动发起时需要传入群号
}
}
throw '请指定 group_id 或 user_id'
@@ -337,12 +342,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
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') {
const uid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
const isBuddy = await NTQQFriendApi.isBuddy(uid!)
@@ -357,7 +356,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
protected async _handle(payload: OB11PostSendMsg) {
const peer = await createContext(payload, ContextMode.Normal)
let contextMode = ContextMode.Normal
if (payload.message_type === 'group') {
contextMode = ContextMode.Group
} else if (payload.message_type === 'private') {
contextMode = ContextMode.Private
}
const peer = await createContext(payload, contextMode)
const messages = convertMessage2List(
payload.message,
payload.auto_escape === true || payload.auto_escape === 'true',
@@ -428,8 +433,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
}
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
return { message_id: returnMsg.msgShortId! }
}
@@ -462,7 +465,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
sendElements,
true,
)
await sleep(500)
await sleep(400)
return nodeMsg
} catch (e) {
log(e, '克隆转发消息失败,将忽略本条消息', msg)
@@ -477,25 +480,17 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用
let needClone =
messageNodes.filter((node) => node.data.id).length && messageNodes.filter((node) => !node.data.id).length
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
let nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId))
if (!needClone) {
nodeMsgIds.push(nodeMsg?.msgId!)
}
else {
if (nodeMsg?.peerUid !== selfPeer.peerUid) {
const cloneMsg = await this.cloneMsg(nodeMsg!)
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId)
}
}
const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId)
if (!nodeMsg) {
log('转发消息失败,未找到消息', nodeId)
continue
}
nodeMsgIds.push(nodeMsg.MsgId)
}
else {
// 自定义的消息
@@ -529,7 +524,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(selfPeer, eles, [], true)
nodeMsgIds.push(nodeMsg.msgId)
await sleep(500)
await sleep(400)
log('转发节点生成成功', nodeMsg.msgId)
}
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
@@ -541,33 +536,25 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
let nodeMsgArray: Array<RawMessage> = []
const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer | null = null
let needSendSelf = false
for (const [index, msgId] of nodeMsgIds.entries()) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId)
if (nodeMsg) {
nodeMsgArray.push(nodeMsg)
if (!srcPeer) {
srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
}
else if (srcPeer.peerUid !== nodeMsg.peerUid) {
for (const msgId of nodeMsgIds) {
const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId)
if (nodeMsgPeer) {
const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]
srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
srcPeer = selfPeer
}
nodeMsgArray.push(nodeMsg)
}
}
log('nodeMsgArray', nodeMsgArray)
nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId)
if (needSendSelf) {
log('需要克隆转发消息')
for (const [index, msg] of nodeMsgArray.entries()) {
if (msg.peerUid !== selfPeer.peerUid) {
const cloneMsg = await this.cloneMsg(msg)
if (cloneMsg) {
nodeMsgIds[index] = cloneMsg.msgId
}
}
for (const msg of nodeMsgArray) {
if (msg.peerUid === selfPeer.peerUid) continue
await this.cloneMsg(msg)
}
}
// elements之间用换行符分隔
@@ -584,7 +571,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
throw Error('转发消息失败,节点为空')
}
const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg)
returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId)
return returnMsg
}
}

View File

@@ -1,32 +1,36 @@
import { ActionName } from '../types'
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 {
message_id: number
emoji_id: string
message_id: number | string
emoji_id: number | string
}
export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike
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) {
throw new Error('msg not found')
}
if (!payload.emoji_id) {
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(
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
msg.msgSeq,
payload.emoji_id,
true,
msg.Peer,
msgData[0].msgSeq,
payload.emoji_id.toString(),
true
)
}
}

View File

@@ -4,12 +4,12 @@
import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from '../types'
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { dbUtil } from '@/common/db'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils'
import { getConfigUtil } from '@/common/config'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface QuickOperationPrivateMessage {
@@ -61,7 +61,6 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc
}
async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11
const peer: Peer = {
@@ -105,17 +104,21 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
}
if (msg.message_type === 'group') {
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
const rawMessage = await MessageUnique.getMsgIdAndPeerByShortId(+(msg.message_id ?? 0))
if (!rawMessage) return
// handle group msg
if (groupMsgQuickAction.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage?.msgId!]).then().catch(log)
NTQQMsgApi.recallMsg(peer, [rawMessage.MsgId]).then().catch(log)
}
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) {
const { msgList } = await NTQQMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
NTQQGroupApi.banMember(peer.peerUid, [
{
uid: rawMessage?.senderUid!,
uid: msgList[0].senderUid,
timeStamp: groupMsgQuickAction.ban_duration || 60 * 30,
},
]).then().catch(log)

View File

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

View File

@@ -15,8 +15,8 @@ export default class SendLike extends BaseAction<Payload, null> {
const qq = payload.user_id.toString()
const uid: string = await NTQQUserApi.getUidByUin(qq) || ''
const result = await NTQQUserApi.like(uid, parseInt(payload.times?.toString()) || 1)
if (result.result !== 0) {
throw Error(result.errMsg)
if (result?.result !== 0) {
throw Error(result?.errMsg)
}
} catch (e) {
throw `点赞失败 ${e}`

View File

@@ -17,20 +17,18 @@ import {
Group,
Peer,
GroupMember,
PicType,
RawMessage,
SelfInfo,
Sex,
TipGroupElementType,
User,
VideoElement,
FriendV2,
ChatType2
} from '../ntqqapi/types'
import { deleteGroup, getGroupMember, getSelfUin } from '../common/data'
import { getGroupMember, getSelfUin } from '../common/data'
import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db'
import { MessageUnique } from '../common/utils/MessageUnique'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
@@ -100,7 +98,7 @@ export class OB11Constructor {
else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group'
const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid)
if (ret.result === 0) {
if (ret?.result === 0) {
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
} else {
@@ -152,115 +150,121 @@ export class OB11Constructor {
}
else if (element.replyElement) {
message_data['type'] = OB11MessageDataType.reply
// log("收到回复消息", element.replyElement.replayMsgSeq)
try {
const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) {
message_data['data']['id'] = replyMsg.msgShortId?.toString()
const records = msg.records.find(msgRecord => msgRecord.msgId === element.replyElement.sourceMsgIdInRecords)
if (!records) throw new Error('找不到回复消息')
let replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({
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 {
continue
// 284840486: 合并消息内侧 消息具体定位不到
if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') {
throw new Error('回复消息消息验证失败')
}
message_data['data']['id'] = replyMsg && MessageUnique.createMsg({
peerUid: msg.peerUid,
guildId: '',
chatType: msg.chatType,
}, replyMsg.msgId)?.toString()
} catch (e: any) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
continue
}
}
else if (element.picElement) {
message_data['type'] = OB11MessageDataType.image
// message_data["data"]["file"] = element.picElement.sourcePath
let fileName = element.picElement.fileName
const sourcePath = element.picElement.sourcePath
const isGif = element.picElement.picType === PicType.gif
const { picElement } = element
/*let fileName = picElement.fileName
const isGif = picElement.picType === PicType.gif
if (isGif && !fileName.endsWith('.gif')) {
fileName += '.gif'
}
message_data['data']['file'] = fileName
message_data['data']['subType'] = element.picElement.picSubType
// message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement)
// message_data["data"]["file_id"] = element.picElement.fileUuid
message_data['data']['file_size'] = element.picElement.fileSize
dbUtil
.addFileCache(fileName, {
fileName,
elementId: element.elementId,
filePath: sourcePath,
fileSize: element.picElement.fileSize.toString(),
url: message_data['data']['url'],
downloadFunc: async () => {
await NTQQFileApi.downloadMedia(
msg.msgId,
msg.chatType,
msg.peerUid,
element.elementId,
element.picElement.thumbPath?.get(0) || '',
element.picElement.sourcePath,
)
},
}).then()
}*/
message_data['data']['file'] = picElement.fileName
message_data['data']['subType'] = picElement.picSubType
//message_data['data']['file_id'] = picElement.fileUuid
message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement)
message_data['data']['file_size'] = picElement.fileSize
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: picElement.fileName,
fileSize: String(picElement.fileSize || '0'),
fileUuid: picElement.fileUuid
})
}
else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file
message_data['type'] = ob11MessageDataType
message_data['data']['file'] = videoOrFileElement.fileName
message_data['data']['path'] = videoOrFileElement.filePath
message_data['data']['file_id'] = videoOrFileElement.fileUuid
message_data['data']['file_size'] = videoOrFileElement.fileSize
if (element.videoElement) {
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId, element.elementId,
)
}
dbUtil
.addFileCache(videoOrFileElement.fileUuid!, {
msgId: msg.msgId,
elementId: element.elementId,
fileName: videoOrFileElement.fileName,
filePath: videoOrFileElement.filePath,
fileSize: videoOrFileElement.fileSize!,
downloadFunc: async () => {
await NTQQFileApi.downloadMedia(
msg.msgId,
msg.chatType,
msg.peerUid,
element.elementId,
ob11MessageDataType == OB11MessageDataType.video
? (videoOrFileElement as VideoElement).thumbPath?.get(0)
: null,
videoOrFileElement.filePath,
)
},
})
.then()
// 怎么拿到url呢
else if (element.videoElement) {
message_data['type'] = OB11MessageDataType.video
const { videoElement } = element
message_data['data']['file'] = videoElement.fileName
message_data['data']['path'] = videoElement.filePath
//message_data['data']['file_id'] = videoElement.fileUuid
message_data['data']['file_size'] = videoElement.fileSize
message_data['data']['url'] = await NTQQFileApi.getVideoUrl({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId, element.elementId)
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: videoElement.fileName,
fileSize: String(videoElement.fileSize || '0'),
fileUuid: videoElement.fileUuid!
})
}
else if (element.fileElement) {
message_data['type'] = OB11MessageDataType.file
const { fileElement } = element
message_data['data']['file'] = fileElement.fileName
message_data['data']['path'] = fileElement.filePath
message_data['data']['file_id'] = fileElement.fileUuid
message_data['data']['file_size'] = fileElement.fileSize
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: fileElement.fileName,
fileSize: String(fileElement.fileSize || '0'),
fileUuid: fileElement.fileUuid!
})
}
else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice
message_data['data']['file'] = element.pttElement.fileName
message_data['data']['path'] = element.pttElement.filePath
// message_data["data"]["file_id"] = element.pttElement.fileUuid
message_data['data']['file_size'] = element.pttElement.fileSize
dbUtil
.addFileCache(element.pttElement.fileName, {
elementId: element.elementId,
fileName: element.pttElement.fileName,
filePath: element.pttElement.filePath,
fileSize: element.pttElement.fileSize,
})
.then()
// log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
// console.log("语音转文字结果", text)
// }).catch(err => {
// console.log("语音转文字失败", err)
// })
const { pttElement } = element
message_data['data']['file'] = pttElement.fileName
message_data['data']['path'] = pttElement.filePath
//message_data['data']['file_id'] = pttElement.fileUuid
message_data['data']['file_size'] = pttElement.fileSize
MessageUnique.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
chatType: msg.chatType,
elementId: element.elementId,
elementType: element.elementType,
fileName: pttElement.fileName,
fileSize: String(pttElement.fileSize || '0'),
fileUuid: pttElement.fileUuid
})
}
else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json
@@ -374,7 +378,7 @@ export class OB11Constructor {
const groupElement = grayTipElement?.groupElement
if (groupElement) {
// log("收到群提示消息", groupElement)
if (groupElement.type == TipGroupElementType.memberIncrease) {
if (groupElement.type === TipGroupElementType.memberIncrease) {
log('收到群成员增加消息', groupElement)
await sleep(1000)
const member = await getGroupMember(msg.peerUid, groupElement.memberUid)
@@ -422,25 +426,26 @@ export class OB11Constructor {
)
}
}
else if (groupElement.type == TipGroupElementType.kicked) {
else if (groupElement.type === TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
deleteGroup(msg.peerUid)
NTQQGroupApi.quitGroup(msg.peerUid).then()
const selfUin = getSelfUin()
try {
const adminUin =
(await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin
const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await NTQQUserApi.getUidByUin(groupElement.adminUid))
if (adminUin) {
return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(selfUin),
parseInt(getSelfUin()),
parseInt(adminUin),
'kick_me',
'kick_me'
)
}
} catch (e) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfUin), 0, 'leave')
return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(getSelfUin()),
0,
'leave'
)
}
}
}
@@ -474,16 +479,27 @@ export class OB11Constructor {
const senderUin = emojiLikeData.gtip.qq.jp
const msgSeq = emojiLikeData.gtip.url.msgseq
const emojiId = emojiLikeData.gtip.face.id
const msg = await dbUtil.getMsgBySeqId(msgSeq)
if (!msg) {
const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({
chatType: ChatType.group,
guildId: '',
peerUid: msg.peerUid,
}, msgSeq, 1, true, true))?.msgList
if (!replyMsgList?.length) {
return
}
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), msg.msgShortId!, [
const likes = [
{
emoji_id: emojiId,
count: 1,
},
])
]
const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid),
parseInt(senderUin),
shortId!,
likes
)
} catch (e: any) {
log('解析表情回应消息失败', e.stack)
}
@@ -556,20 +572,26 @@ export class OB11Constructor {
const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode')
const Businessid = searchParams.get('businessid')
const Peer: Peer = {
guildId: '',
chatType: ChatType.group,
peerUid: Group!
}
let msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)).msgList
const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
if (msgList[0].senderUin === '0') {
msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin()
const msgList = (await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true))?.msgList
if (!msgList?.length) {
return
}
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin!))
//const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
//const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
//if (msgList[0].senderUin === '0') {
//msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin()
//}
return new OB11GroupEssenceEvent(
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgList[0].msgId)!,
parseInt(msgList[0].senderUin!)
)
// 获取MsgSeq+Peer可获取具体消息
}
if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
@@ -590,27 +612,26 @@ export class OB11Constructor {
static async RecallEvent(
msg: RawMessage,
shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
let msgElement = msg.elements.find(
const msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL,
)
if (!msgElement) {
return
}
const isGroup = msg.chatType === ChatType.group
const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) {
if (msg.chatType === ChatType.group) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid!)
return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid),
parseInt(sender?.uin!),
parseInt(operator?.uin!),
msg.msgShortId!,
parseInt(msg.senderUin!),
parseInt(operator?.uin || msg.senderUin!),
shortId,
)
}
else {
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), msg.msgShortId!)
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin!), shortId)
}
}
@@ -680,7 +701,7 @@ export class OB11Constructor {
sex: OB11Constructor.sex(member.sex!),
age: 0,
area: '',
level: 0,
level: '0',
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -219,6 +219,11 @@ async function onSettingWindowCreated(view: Element) {
`${window.LiteLoader.plugins['LLOneBot'].path.data}/logs`,
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([
SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')),

View File

@@ -1 +1 @@
export const version = '3.28.5'
export const version = '3.30.0'