Compare commits

...

64 Commits

Author SHA1 Message Date
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
idranme
a0f5cc0e36 Merge pull request #333 from LLOneBot/dev
Update README.md
2024-08-12 15:01:26 +08:00
idranme
277c2a9b67 Update README.md 2024-08-12 15:00:41 +08:00
idranme
874acdd7fe Merge pull request #331 from LLOneBot/dev
3.28.6
2024-08-12 00:03:25 +08:00
idranme
b2b996df9c chore: v3.28.6 2024-08-12 00:01:39 +08:00
idranme
4427774c2d fix: multiForwardMsg 2024-08-12 00:01:06 +08:00
idranme
41c04faa05 Merge pull request #330 from LLOneBot/dev
3.28.5
2024-08-11 20:01:29 +08:00
idranme
6ad4492f01 chore: v3.28.5 2024-08-11 20:00:47 +08:00
idranme
d52f16bc88 opt 2024-08-11 19:42:44 +08:00
idranme
2b0179acd1 opt 2024-08-11 18:10:27 +08:00
idranme
f540f324a1 Merge pull request #329 from LLOneBot/dev
3.28.4
2024-08-11 12:21:37 +08:00
idranme
128f40a51d chore: v3.28.4 2024-08-11 12:17:47 +08:00
idranme
c815e0ca6b sync 2024-08-11 12:16:53 +08:00
idranme
1da720e0a7 sync 2024-08-11 02:43:14 +08:00
idranme
1472c9c949 opt 2024-08-11 00:23:17 +08:00
idranme
4678253815 sync 2024-08-11 00:18:54 +08:00
idranme
e1176e18cd Merge pull request #328 from LLOneBot/dev
3.28.3
2024-08-10 23:19:09 +08:00
idranme
107f02f21f chore: 3.28.3 2024-08-10 23:17:38 +08:00
idranme
51f8db3a83 opt 2024-08-10 22:31:14 +08:00
idranme
25691a4124 sync 2024-08-10 22:09:35 +08:00
idranme
40f03e6401 sync 2024-08-10 21:34:28 +08:00
idranme
9f89094978 sync 2024-08-10 20:36:15 +08:00
idranme
04f837145c sync 2024-08-10 18:14:33 +08:00
idranme
6126920830 sync 2024-08-10 17:17:19 +08:00
idranme
5c219aa003 opt 2024-08-09 22:32:54 +08:00
idranme
ce5cf82339 Merge pull request #325 from LLOneBot/dev
3.28.2
2024-08-09 18:10:50 +08:00
idranme
6931277e33 chore: v3.28.2 2024-08-09 18:07:50 +08:00
idranme
be1b9c21c1 feat: support for at message segment specifying name 2024-08-09 18:02:52 +08:00
idranme
b02cd3af00 Create .editorconfig 2024-08-09 16:46:08 +08:00
89 changed files with 2786 additions and 2470 deletions

9
.editorconfig Normal file
View File

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

View File

@@ -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.1",
"description": "实现 OneBot 11 协议,用 QQ 机器人开发",
"version": "3.29.6",
"icon": "./icon.webp",
"authors": [
{
@@ -13,7 +13,7 @@
}
],
"repository": {
"repo": "linyuchen/LiteLoaderQQNT-OneBotApi",
"repo": "LLOneBot/LLOneBot",
"branch": "main",
"release": {
"tag": "latest",

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,15 @@
"author": "",
"license": "MIT",
"dependencies": {
"@minatojs/driver-sqlite": "^4.5.0",
"compressing": "^1.10.1",
"cordis": "^3.18.0",
"cors": "^2.8.5",
"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"
},
@@ -30,12 +32,12 @@
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.11.24",
"@types/node": "^20.14.15",
"@types/ws": "^8.5.12",
"electron": "^29.0.1",
"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: [
@@ -16,7 +16,7 @@ const manifest = {
}
],
repository: {
repo: 'linyuchen/LiteLoaderQQNT-OneBotApi',
repo: 'LLOneBot/LLOneBot',
branch: 'main',
release: {
tag: 'latest',

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

@@ -2,12 +2,10 @@ import fs from 'node:fs'
import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper'
import path from 'node:path'
import { selfInfo } from './data'
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)) {
@@ -97,6 +96,6 @@ export class ConfigUtil {
}
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
const configFilePath = path.join(DATA_DIR, `config_${getSelfUin()}.json`)
return new ConfigUtil(configFilePath)
}

View File

@@ -1,46 +1,32 @@
import {
CategoryFriend,
type Friend,
type FriendRequest,
type Group,
type GroupMember,
type SelfInfo,
User,
} from '../ntqqapi/types'
import { type FileCache, type LLOneBotError } from './types'
import { type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log'
import { isNumeric } from './utils/helper'
import { NTQQFriendApi } from '../ntqqapi/api'
import { WebApiGroupMember } from '@/ntqqapi/api/webapi'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
import { getBuildVersion } from './utils/QQBasicInfo'
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export const WebGroupData = {
GroupData: new Map<string, Array<WebApiGroupMember>>(),
GroupTime: new Map<string, number>(),
}
export let groups: Group[] = []
export let friends: Friend[] = []
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
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())
@@ -54,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
@@ -94,31 +56,73 @@ 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
}
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) {
for (const uid in uidMaps) {
if (uidMaps[uid] === uin) {
return uid
}
}
const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号
export async function getSelfNick(force = false): Promise<string> {
if ((!selfInfo.nick || force) && selfInfo.uid) {
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
selfInfo.nick = userInfo.nick
return userInfo.nick
}
}
export let rawFriends: CategoryFriend[] = []
return selfInfo.nick
}
export function getSelfInfo() {
return selfInfo
}
export function setSelfInfo(data: Partial<SelfInfo>) {
Object.assign(selfInfo, data)
}
export function getSelfUid() {
return selfInfo['uid']
}
export function getSelfUin() {
return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
/** 缓存近期消息内容 */
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,275 +0,0 @@
import { Level } from 'level'
import { type GroupNotify, RawMessage } from '../ntqqapi/types'
import { DATA_DIR } from './utils'
import { selfInfo } from './data'
import { FileCache } from './types'
import { log } from './utils/log'
type ReceiveTempUinMap = Record<string, string>
class DBUtil {
public readonly DB_KEY_PREFIX_MSG_ID = 'msg_id_'
public readonly DB_KEY_PREFIX_MSG_SHORT_ID = 'msg_short_id_'
public readonly DB_KEY_PREFIX_MSG_SEQ_ID = 'msg_seq_id_'
public readonly DB_KEY_PREFIX_FILE = 'file_'
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = 'group_notify_'
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = 'received_temp_uin_map'
public db: Level | undefined
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number | undefined
/*
* 数据库结构
* msg_id_101231230999: {} // 长id: RawMessage
* msg_short_id_1: 101231230999 // 短id: 长id
* msg_seq_id_1: 101231230999 // 序列id: 长id
* file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */
constructor() {
let initCount = 0
new Promise((resolve, reject) => {
const initDB = () => {
initCount++
// if (initCount > 50) {
// return reject("init db fail")
// }
try {
if (!selfInfo.uin) {
setTimeout(initDB, 300)
return
}
const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}`
this.db = new Level(DB_PATH, { valueEncoding: 'json' })
console.log('llonebot init db success')
resolve(null)
} catch (e: any) {
console.log('init db fail', e.stack.toString())
setTimeout(initDB, 300)
}
}
setTimeout(initDB)
}).then()
const expiredMilliSecond = 1000 * 60 * 60
setInterval(() => {
// this.cache = {}
// 清理时间较久的缓存
const now = Date.now()
for (let key in this.cache) {
let message: RawMessage = this.cache[key] as RawMessage
if (message?.msgTime) {
if (now - parseInt(message.msgTime) * 1000 > expiredMilliSecond) {
delete this.cache[key]
// log("clear cache", key, message.msgTime);
}
}
}
}, expiredMilliSecond)
}
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db?.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP)!)
} catch (e) { }
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data
this.db?.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then()
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
this.cache[longIdKey] = this.cache[shortIdKey] = msg
}
public clearCache() {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage | undefined> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try {
const longId = await this.db?.get(shortMsgIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e: any) {
log('getMsgByShortId db error', e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage | undefined> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db?.get(longIdKey)
const msg = JSON.parse(data!)
this.addCache(msg)
return msg
} catch (e) {
// log("getMsgByLongId db error", e.stack.toString())
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage | undefined> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db?.get(seqIdKey)
const msg = await this.getMsgByLongId(longId!)
this.addCache(msg!)
return msg
} catch (e: any) {
log('getMsgBySeqId db error', e.stack.toString())
}
}
async addMsg(msg: RawMessage) {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
// log("addMsg getMsgByLongId error", e.stack.toString())
}
}
if (existMsg) {
// log("消息已存在", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId)
this.updateMsg(msg).then()
return existMsg.msgShortId
}
const shortMsgId = await this.genMsgShortId()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
msg.msgShortId = shortMsgId
this.addCache(msg)
// log("新增消息记录", msg.msgId)
this.db?.put(shortIdKey, msg.msgId).then().catch()
this.db?.put(longIdKey, JSON.stringify(msg)).then().catch()
try {
await this.db?.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db?.put(seqIdKey, msg.msgId).then().catch()
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg
}
return shortMsgId
// log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`);
}
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg: RawMessage | undefined = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
existMsg = msg
}
}
Object.assign(existMsg!, msg)
this.db?.put(longIdKey, JSON.stringify(existMsg)).then().catch()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg?.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg!
}
this.db?.put(shortIdKey, msg.msgId).then().catch()
try {
await this.db?.get(seqIdKey)
} catch (e) {
this.db?.put(seqIdKey, msg.msgId).then().catch()
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
}
private async genMsgShortId(): Promise<number> {
const key = 'msg_current_short_id'
if (this.currentShortId === undefined) {
try {
const id = await this.db?.get(key)
this.currentShortId = parseInt(id!)
} catch (e) {
this.currentShortId = -2147483640
}
}
this.currentShortId++
this.db?.put(key, this.currentShortId.toString()).then().catch()
return this.currentShortId
}
async addFileCache(fileNameOrUuid: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) {
return
}
let cacheDBData = { ...data }
delete cacheDBData['downloadFunc']
this.cache[fileNameOrUuid] = data
try {
await this.db?.put(key, JSON.stringify(cacheDBData))
} catch (e: any) {
log('addFileCache db error', e.stack.toString())
}
}
async getFileCache(fileNameOrUuid: string): Promise<FileCache | undefined> {
const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid
if (this.cache[key]) {
return this.cache[key] as FileCache
}
try {
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getFileCache db error", e.stack.toString())
}
}
async addGroupNotify(notify: GroupNotify) {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + notify.seq
let existNotify = this.cache[key] as GroupNotify
if (existNotify) {
return
}
this.cache[key] = notify
this.db?.put(key, JSON.stringify(notify)).then().catch()
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + seq
if (this.cache[key]) {
return this.cache[key] as GroupNotify
}
try {
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}
}
}
export const dbUtil = new DBUtil()

View File

@@ -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,6 +16,7 @@ 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
@@ -33,7 +34,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()
}
}
// 如果方法存在,正常返回
@@ -47,7 +48,7 @@ export class NTEventWrapper {
this.WrapperSession = WrapperSession
}
CreatEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/')
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
@@ -68,14 +69,14 @@ export class NTEventWrapper {
}
}
CreatListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
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 +85,7 @@ export class NTEventWrapper {
}
//统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout)
@@ -100,7 +101,7 @@ export class NTEventWrapper {
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
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 +150,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 +196,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,5 +1,4 @@
import path from 'node:path'
import fs from 'node:fs'
import os from 'node:os'
import { systemPlatform } from './system'
@@ -38,32 +37,6 @@ type QQPkgInfo = {
platform: string
eleArch: string
}
type QQVersionConfigInfo = {
baseVersion: string
curVersion: string
prevVersion: string
onErrorVersions: Array<any>
buildId: string
}
let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.9-23361',
'curVersion': '9.9.9-23361',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '23361',
}
if (fs.existsSync(configVersionInfoPath)) {
try {
const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString())
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _)
} catch (e) {
console.error('Load QQ version config info failed, Use default version', e)
}
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3,
@@ -74,14 +47,6 @@ export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
let _appid: string = '537213803' // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') {
_appid = '537213827'
}
// todo: mac 平台的 appid
export const appid = _appid
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'
export function getBuildVersion(): number {
return +qqPkgInfo.buildVersion
}

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)
@@ -25,7 +24,7 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`))
} else {
setTimeout(check, 100)
setTimeout(check, 200)
}
}
@@ -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

@@ -96,6 +96,29 @@ export function cacheFunc(ttl: number, customKey: string = '') {
}
}
export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
@@ -120,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,13 +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'
export { isQQ998 } from './QQBasicInfo'
export { encodeSilk } from './audio'

View File

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

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,7 +1,8 @@
// 运行在 Electron 主进程 下的插件入口
import { BrowserWindow, dialog, ipcMain } from 'electron'
import * as fs from 'node:fs'
import path from 'node:path'
import fs from 'node:fs'
import { Config } from '../common/types'
import {
CHANNEL_CHECK_VERSION,
@@ -13,52 +14,46 @@ import {
CHANNEL_UPDATE,
} from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR, qqPkgInfo } from '../common/utils'
import { DATA_DIR, TEMP_DIR } from '../common/utils'
import {
friendRequests,
getFriend,
getGroup,
getGroupMember,
groups,
llonebotError,
selfInfo,
uidMaps,
setSelfInfo,
getSelfInfo,
getSelfUid,
getSelfUin,
addMsgCache
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import {
ChatType,
FriendRequestNotify,
GroupMemberRole,
GroupNotifies,
GroupNotify,
GroupNotifyTypes,
RawMessage,
BuddyReqType,
} from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
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 * as path from 'node:path'
import { dbUtil } from '../common/db'
import { MessageUnique } from '../common/utils/MessageUnique'
import { setConfig } from './setConfig'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/wrapper'
import { sentMessages } from '@/ntqqapi/api'
import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperConstructor, getSession } from '../ntqqapi/wrapper'
import { Peer } from '../ntqqapi/types'
let mainWindow: BrowserWindow | null = null
// 加载插件时触发
function onLoad() {
log('llonebot main onLoad')
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkNewVersion()
})
@@ -100,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')
@@ -154,21 +149,23 @@ 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) => {
if (!debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
const isSelfMsg = msg.user_id.toString() === getSelfUin()
if (isSelfMsg && !reportSelfMessage) {
return
}
@@ -186,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)
// }
// })
}
}
@@ -216,33 +206,22 @@ function onLoad() {
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
const sentMessage = sentMessages[message.msgId]
if (sentMessage) {
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message)
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
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) => {
@@ -257,6 +236,7 @@ function onLoad() {
log('report self message error: ', e.stack.toString())
}
})
const processedGroupNotify: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
@@ -264,131 +244,87 @@ 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)
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
// 原本的群管变更通知事件处理
// if (
// [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
// notify.type,
// )
// ) {
// const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
// log('有管理员变动通知')
// refreshGroupMembers(notify.group.groupCode).then()
// let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
// groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
// log('开始获取变动的管理员')
// if (member1) {
// log('变动管理员获取成功')
// groupAdminNoticeEvent.user_id = parseInt(member1.uin)
// groupAdminNoticeEvent.sub_type = [
// GroupNotifyTypes.ADMIN_UNSET,
// GroupNotifyTypes.ADMIN_UNSET_OTHER,
// ].includes(notify.type)
// ? 'unset'
// : 'set'
// // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
// postOb11Event(groupAdminNoticeEvent, true)
// }
// else {
// log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
// }
// }
// else
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('有加群请求')
let requestQQ = uidMaps[notify.user1.uid]
if (!requestQQ) {
try {
let requestQQ = ''
try {
// uid-->uin
requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid))
if (isNaN(parseInt(requestQQ))) {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
}
} catch (e) {
log('获取加群人QQ号失败 Uid:', notify.user1.uid, e)
}
let invitorId: number
let invitorId: string
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid]
if (!invitorQQ) {
try {
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))
invitorId = parseInt(invitor.uin)
} catch (e) {
invitorId = 0
log('获取邀请人QQ号失败', e)
try {
// uid-->uin
invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid))
if (isNaN(parseInt(invitorId))) {
invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin
}
} catch (e) {
invitorId = ''
log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e)
}
}
const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0,
notify.seq,
flag,
notify.postscript,
invitorId!,
invitorId! === undefined ? undefined : +invitorId,
'add'
)
postOb11Event(groupRequestEvent)
}
else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
let userId = uidMaps[notify.user2.uid]
if (!userId) {
userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || ''
const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId),
notify.seq,
flag,
undefined,
undefined,
'invite'
@@ -407,44 +343,50 @@ function onLoad() {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
const flag = req.friendUid + req.reqTime
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) {
friendRequests[flag] = req
log('有新的好友请求', req)
let userId: number
try {
const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
userId = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag)
postOb11Event(friendRequestEvent)
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
continue
}
let userId = 0
try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin!)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
postOb11Event(friendRequestEvent)
}
})
}
let startTime = 0 // 毫秒
async function start() {
log('llonebot pid', process.pid)
async function start(uid: string, uin: string) {
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()
dbUtil.getReceivedTempUinMap().then((m) => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key
}
})
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! })
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log)
MessageUnique.init(uin)
//log('start activate group member info')
// 下面两个会导致CPU占用过高QQ卡死
// NTQQGroupApi.activateMemberInfoChange().then().catch(log)
// NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
if (config.ob11.enableHttp) {
@@ -463,54 +405,25 @@ function onLoad() {
log('LLOneBot start')
}
let getSelfNickCount = 0
const init = async () => {
try {
log('start get self info')
const _ = await NTQQUserApi.getSelfInfo()
log('get self info api result:', _)
Object.assign(selfInfo, _)
selfInfo.nick = selfInfo.uin
} catch (e) {
log('retry get self info', e)
const intervalId = setInterval(() => {
const current = getSelfInfo()
if (!current.uin) {
setSelfInfo({
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
nick: current.uin,
})
}
if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin
selfInfo.uid = globalThis.authData?.uid
selfInfo.nick = selfInfo.uin
if (current.uin && getSession()) {
clearInterval(intervalId)
start(current.uid, current.uin)
}
log('self info', selfInfo, globalThis.authData)
if (selfInfo.uin) {
async function getUserNick() {
try {
getSelfNickCount++
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
log('self info', userInfo)
if (userInfo) {
selfInfo.nick = userInfo.nick
return
}
} catch (e: any) {
log('get self nickname failed', e.stack)
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000)
}
}
getUserNick().then()
start().then()
}
else {
setTimeout(init, 1000)
}
}
setTimeout(init, 1000)
}, 600)
}
// 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) {
if (getSelfUid()) {
return
}
mainWindow = window

View File

@@ -9,15 +9,22 @@ import {
ChatType,
ElementType,
IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement,
IMAGE_HTTP_HOST_NT,
PicElement,
} from '../types'
import path from 'node:path'
import fs from 'node:fs'
import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils'
import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { NodeIKernelSearchService } from '@/ntqqapi/services'
export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
@@ -26,23 +33,11 @@ export class NTQQFileApi {
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 await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_TYPE,
args: [filePath],
})
}
static async getFileMd5(filePath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath],
})
return fileTypeFromFile(filePath)
}
static async copyFile(filePath: string, destPath: string) {
@@ -67,44 +62,35 @@ export class NTQQFileApi {
}
// 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext || ''
if (ext) {
ext = '.' + ext
} else {
ext = ''
}
let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) {
fileName += ext
}
const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [
{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
},
],
const session = getSession()
const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
})
log('media path', mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath)
const fileSize = await NTQQFileApi.getFileSize(filePath)
await fsPromise.copyFile(filePath, mediaPath!)
const fileSize = (await fsPromise.stat(filePath)).size
return {
md5,
md5: fileMd5,
fileName,
path: mediaPath,
path: mediaPath!,
fileSize,
ext,
ext
}
}
@@ -115,44 +101,67 @@ export class NTQQFileApi {
elementId: string,
thumbPath: string,
sourcePath: string,
force: boolean = false,
timeout = 1000 * 60 * 2,
force = false
) {
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) {
if (force) {
fs.unlinkSync(sourcePath)
try {
await fsPromise.unlink(sourcePath)
} catch (e) {
//
}
} else {
return sourcePath
}
}
const apiParams = [
const data = await NTEventDispatch.CallNormalEvent<
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{
getReq: {
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => {
log('media 下载完成判断', payload.notifyInfo.msgId, msgId)
return payload.notifyInfo.msgId == msgId
},
})
return sourcePath
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
if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath)
// 下载路径是下载文件夹的相对路径
}
return filePath
}
static async getImageSize(filePath: string) {
@@ -163,22 +172,26 @@ export class NTQQFileApi {
})
}
static async getImageUrl(picElement: PicElement, chatType: ChatType) {
const isPrivateImage = chatType !== ChatType.group
const url = picElement.originImageUrl // 没有域名
const md5HexStr = picElement.md5HexStr
const fileMd5 = picElement.md5HexStr
const fileUuid = picElement.fileUuid
static async getImageUrl(element: PicElement) {
if (!element) {
return ''
}
const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr
if (url) {
if (url.startsWith('/download')) {
// console.log('rkey', rkey);
if (url.includes('&rkey=')) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = UrlParse.searchParams.get('appid')
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey')
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url
}
const rkeyData = await rkeyManager.getRkey();
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
const rkeyData = await rkeyManager.getRkey()
UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url
@@ -187,9 +200,125 @@ export class NTQQFileApi {
// 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
}
log('图片url获取失败', picElement)
log('图片url获取失败', element)
return ''
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/core/src/apis/file.ts#L149
static async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) {
let GroupData: any[] | undefined
let BuddyData: any[] | undefined
if (peer.chatType === ChatType.group) {
GroupData =
[{
groupCode: peer.peerUid,
isConf: false,
hasModifyConfGroupFace: true,
hasModifyConfGroupName: true,
groupName: 'LLOneBot.Cached',
remark: 'LLOneBot.Cached',
}];
} else if (peer.chatType === ChatType.friend) {
BuddyData = [{
category_name: 'LLOneBot.Cached',
peerUid: peer.peerUid,
peerUin: peer.peerUid,
remark: 'LLOneBot.Cached',
}]
} else {
return undefined
}
const session = getSession()
return session?.getSearchService().addSearchHistory({
type: 4,
contactList: [],
id: -1,
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: peer.chatType,
buddyChatInfo: BuddyData || [],
discussChatInfo: [],
groupChatInfo: GroupData || [],
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: msgId,
msgSeq: msgSeq,
msgTime: Math.floor(Date.now() / 1000).toString(),
senderUid: senderUid,
senderNick: 'LLOneBot.Cached',
senderRemark: 'LLOneBot.Cached',
senderCard: 'LLOneBot.Cached',
elemId: elemId,
elemType: elemType,
fileSize: fileSize,
filePath: '',
fileName: fileName,
hits: [{
start: 12,
end: 14,
}],
},
],
})
}
static async searchfile(keys: string[]) {
type EventType = NodeIKernelSearchService['searchFileWithKeywords']
interface OnListener {
searchId: string,
hasMore: boolean,
resultItems: {
chatType: ChatType,
buddyChatInfo: any[],
discussChatInfo: any[],
groupChatInfo:
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}[],
dataLineChatInfo: any[],
tmpChatInfo: any[],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: number,
fileSize: string,
filePath: string,
fileName: string,
hits:
{
start: number,
end: number
}[]
}[]
}
const Event = NTEventDispatch.createEventFunction<EventType>('NodeIKernelSearchService/searchFileWithKeywords')
let id = ''
const Listener = NTEventDispatch.RegisterListen<(params: OnListener) => void>
(
'NodeIKernelSearchListener/onSearchFileKeywordsResult',
1,
20000,
(params) => id !== '' && params.searchId == id,
)
id = await Event!(keys, 12)
const [ret] = await Listener
return ret
}
}
export class NTQQFileCacheApi {

View File

@@ -1,7 +1,6 @@
import { Friend, FriendRequest, FriendV2 } from '../types'
import { Friend, FriendV2 } from '../types'
import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { friendRequests } from '@/common/data'
import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask'
@@ -9,6 +8,7 @@ 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<{
data: {
@@ -49,24 +49,18 @@ export class NTQQFriendApi {
}
static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
throw `flat: ${flag}, 对应的好友请求不存在`
const data = flag.split('|')
if (data.length < 2) {
return
}
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
approvalInfo: {
friendUid: request.friendUid,
reqTime: request.reqTime,
accept,
},
},
],
const friendUid = data[0]
const reqTime = data[1]
const session = getSession()
return session?.getBuddyService().approvalFriendRequest({
friendUid,
reqTime,
accept
})
delete friendRequests[flag]
return result
}
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
@@ -103,6 +97,28 @@ export class NTQQFriendApi {
return retMap
}
static async getBuddyV2ExWithCate(refresh = false) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const session = getSession()
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data : (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data
uids.push(
...buddyListV2?.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName })
})
return item.buddyUids
})!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
}
static async isBuddy(uid: string): Promise<boolean> {
const session = getSession()
return session?.getBuddyService().isBuddy(uid)!

View File

@@ -1,11 +1,11 @@
import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import { deleteGroup, uidMaps } from '../../common/data'
import { dbUtil } from '../../common/db'
import { log } from '../../common/utils/log'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
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() {
@@ -37,22 +37,36 @@ export class NTQQGroupApi {
})
}
static async getGroups(forced = false) {
// let cbCmd = ReceiveCmdS.GROUPS
// if (process.platform != 'win32') {
// cbCmd = ReceiveCmdS.GROUPS_STORE
// }
const result = await callNTQQApi<{
updateType: number
groupList: Group[]
}>({
methodName: NTQQApiMethod.GROUPS,
args: [{ force_update: forced }, undefined],
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE],
afterFirstCmd: false,
})
log('get groups result', result)
return result.groupList
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,
() => true,
forced
)
return groupList
}
static async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
type ListenerType = NodeIKernelGroupListener['onMemberInfoChange']
type EventType = NodeIKernelGroupService['getMemberInfo']
const [, , , _members] = await NTEventDispatch.CallNormalEvent<EventType, ListenerType>
(
'NodeIKernelGroupService/getMemberInfo',
'NodeIKernelGroupListener/onMemberInfoChange',
1,
5000,
(groupCode: string, changeType: number, members: Map<string, GroupMember>) => {
return groupCode == GroupCode && members.has(uid)
},
GroupCode, [uid], forced,
)
return _members.get(uid)
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
@@ -104,130 +118,94 @@ export class NTQQGroupApi {
)
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq]
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
doubt: false,
operateMsg: {
operateType: operateType, // 2 拒绝
targetMsg: {
seq: seq, // 通知序列号
type: notify.type,
groupCode: notify.group.groupCode,
postscript: reason,
},
},
},
null,
],
})
static async getSingleScreenNotifies(num: number) {
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
}
static async delGroupFile(groupCode: string, files: string[]) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)!
}
static DelGroupFile = NTQQGroupApi.delGroupFile
static async delGroupFileFolder(groupCode: string, folderId: string) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFolder(groupCode, folderId)!
}
static DelGroupFileFolder = NTQQGroupApi.delGroupFileFolder
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|')
const groupCode = flagitem[0]
const seq = flagitem[1]
const type = parseInt(flagitem[2])
const session = getSession()
return session?.getGroupService().operateSysNotify(
false,
{
'operateType': operateType, // 2 拒绝
'targetMsg': {
'seq': seq, // 通知序列号
'type': type,
'groupCode': groupCode,
'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}
})
}
static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
if (result.result === 0) {
deleteGroup(groupQQ)
}
return result
const session = getSession()
return session?.getGroupService().quitGroup(groupQQ)
}
static async kickMember(
groupQQ: string,
kickUids: string[],
refuseForever: boolean = false,
kickReason: string = '',
refuseForever = false,
kickReason = '',
) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
},
],
})
const session = getSession()
return session?.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
}
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
},
],
})
const session = getSession()
return session?.getGroupService().setMemberShutUp(groupQQ, memList)
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
const session = getSession()
return session?.getGroupService().setGroupShutUp(groupQQ, shutUp)
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
NTQQGroupApi.activateMemberListChange().then().catch(log)
const res = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res
const session = getSession()
return session?.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
const session = getSession()
return session?.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
const session = getSession()
return session?.getGroupService().modifyGroupName(groupQQ, groupName, false)
}
static async getGroupAtAllRemainCount(groupCode: string) {
@@ -252,19 +230,13 @@ 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) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title,
},
null,
],
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) { }

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { WebGroupData, groups, selfInfo } from '@/common/data'
import { getSelfUin } from '@/common/data'
import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request'
import { CacheClassFuncAsync } from '@/common/utils/helper'
export enum WebHonorType {
ALL = 'all',
@@ -138,56 +139,46 @@ export class WebApi {
}
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode);
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
try {
let cachedData = WebGroupData.GroupData.get(GroupCode);
let cachedTime = WebGroupData.GroupTime.get(GroupCode);
if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) {
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' + selfInfo.uin;
if (!_Skey || !_Pskey) {
return MemberData;
}
const Bkn = WebApi.genBkn(_Skey);
const retList: Promise<WebApiGroupMemberRet>[] = [];
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
} else {
for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key]);
}
}
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40);
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
retList.push(ret);
}
//批量等待
for (let i = 1; i <= PageNum; i++) {
const ret = await (retList[i]);
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue;
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key]);
}
}
WebGroupData.GroupData.set(GroupCode, MemberData);
WebGroupData.GroupTime.set(GroupCode, Date.now());
} else {
MemberData = cachedData as Array<WebApiGroupMember>;
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)
}
} 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);
@@ -203,49 +194,47 @@ export class WebApi {
static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: any = undefined;
//console.log(CookieValue);
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']
const _Skey = await NTQQUserApi.getSkey()
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + getSelfUin()
let ret: any = undefined
//console.log(CookieValue)
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
return undefined
}
const Bkn = WebApi.genBkn(_Skey);
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}';
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn;
const Bkn = WebApi.genBkn(_Skey)
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}'
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn
try {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue });
return ret;
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue })
return ret
} catch (e) {
return undefined;
return undefined
}
return undefined;
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: WebApiGroupNoticeRet | undefined = undefined;
//console.log(CookieValue);
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;
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';
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 });
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue })
if (ret?.ec !== 0) {
return undefined;
return undefined
}
return ret;
return ret
} catch (e) {
return undefined;
return undefined
}
return undefined;
}
static genBkn(sKey: string) {

View File

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

View File

@@ -1,29 +1,30 @@
import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTQQMsgApi, sendMessagePool } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types'
import { NTQQMsgApi } from './api/msg'
import {
CategoryFriend,
ChatType,
GroupMember,
GroupMemberRole,
RawMessage,
SimpleInfo, User,
} from './types'
import {
deleteGroup,
friends,
getFriend,
getGroupMember,
groups, rawFriends,
selfInfo,
tempGroupCodeMap,
uidMaps,
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> = {}
@@ -79,52 +80,41 @@ let callHooks: Array<{
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)
/*try {
const isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
if (!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,9 +134,9 @@ export function hookNTQQApiCall(window: BrowserWindow) {
isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) { }
if (!isLogger) {
try {
/*try {
HOOK_LOG && log('call NTQQ api', thisArg, args)
} catch (e) { }
} catch (e) { }*/
try {
const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod
@@ -180,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
},
})
@@ -234,28 +224,17 @@ 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)
// if (!activatedGroups.includes(group.groupCode)) {
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group })
.then((r) => {
// activatedGroups.push(group.groupCode);
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
})
.catch(log)
// }
//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) {
Object.assign(existGroup, group)
@@ -272,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) {
@@ -295,12 +274,13 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
}
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
const selfUin = getSelfUin()
const bot = await getGroupMember(group.groupCode, selfUin)
if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
if (!newMembersSet.has(member.uin) && member.uin != selfUin) {
postOb11Event(
new OB11GroupDecreaseEvent(
parseInt(group.groupCode),
@@ -324,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) {
@@ -343,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()
}
@@ -352,7 +332,7 @@ export async function startHook() {
processGroupEvent(payload).then()
}
}
})
})*/
registerReceiveHook<{
groupCode: string
@@ -402,41 +382,37 @@ export async function startHook() {
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0;
rawFriends.push(...payload.data);
for (const fData of payload.data) {
const _friends = fData.buddyList
for (let friend of _friends) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
// 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)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
}
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid
const uin = message.senderUin
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin
}
}
// 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) {
@@ -459,10 +435,6 @@ export async function startHook() {
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
@@ -478,23 +450,18 @@ export async function startHook() {
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord
const peerUid = message.peerUid
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message)
} catch (e: any) {
log('receive self msg error', e.stack)
}
const { msgId, chatType, peerUid } = msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20
setSelfInfo({
online: info.info.status !== 20
})
})
let activatedPeerUids: string[] = []
@@ -551,4 +518,4 @@ export async function startHook() {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,4 +4,8 @@ export * from './NodeIKernelGroupService'
export * from './NodeIKernelProfileLikeService'
export * from './NodeIKernelMsgService'
export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService'
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

@@ -1,6 +1,15 @@
import { GroupMemberRole } from './group'
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
}
export enum ElementType {
UNKNOWN = 0,
TEXT = 1,
PIC = 2,
FILE = 3,
@@ -8,8 +17,30 @@ export enum ElementType {
VIDEO = 5,
FACE = 6,
REPLY = 7,
WALLET = 9,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10,
MFACE = 11,
LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
}
export interface SendTextElement {
@@ -101,18 +132,19 @@ export interface ReplyElement {
}
export interface FileElement {
fileMd5?: ''
fileMd5?: string
fileName: string
filePath: string
fileSize: string
picHeight?: number
picWidth?: number
picThumbPath?: {}
file10MMd5?: ''
fileSha?: ''
fileSha3?: ''
fileUuid?: ''
fileSubId?: ''
folderId?: string
picThumbPath?: Map<number, string>
file10MMd5?: string
fileSha?: string
fileSha3?: string
fileUuid?: string
fileSubId?: string
thumbFileSize?: number
fileBizId?: number
}
@@ -158,6 +190,50 @@ export enum ChatType {
temp = 100,
}
// 来自Android分析
export enum ChatType2 {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
}
export interface PttElement {
canConvert2Text: boolean
duration: number // 秒数
@@ -386,15 +462,18 @@ export interface RawMessage {
senderUin?: string // 发送者QQ号
peerUid: string // 群号 或者 QQ uid
peerUin: string // 群号 或者 发送者QQ号
guildId: string
sendNickName: string
sendMemberName?: string // 发送者群名片
chatType: ChatType
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[]
elements: {
elementId: string
elementType: ElementType
replyElement: {
sourceMsgIdInRecords: string
senderUid: string // 原消息发送者QQ号
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string

View File

@@ -48,8 +48,28 @@ export enum GroupRequestOperateTypes {
reject = 2,
}
export enum BuddyReqType {
KMEINITIATOR,
KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
}
export interface FriendRequest {
isInitiator?: boolean
isDecide: boolean
friendUid: string
reqType: BuddyReqType
reqTime: string // 时间戳,秒
extWords: string // 申请人填写的验证消息
isUnread: boolean

View File

@@ -340,4 +340,4 @@ export interface UserDetailInfoByUin {
pendantId: string
vipNameColorId: string
}
}
}

View File

@@ -5,7 +5,11 @@ import {
NodeIKernelProfileLikeService,
NodeIKernelMSFService,
NodeIKernelMsgService,
NodeIKernelUixConvertService
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services'
import os from 'node:os'
const Process = require('node:process')
@@ -19,6 +23,10 @@ export interface NodeIQQNTWrapperSession {
getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService
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, '', '', true)
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg!, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}
}
}
// 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,23 @@ 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[]
// 包含 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 (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,19 +1,59 @@
import BaseAction from '../BaseAction'
import { OB11User } from '../../types'
import { getUidByUin, uidMaps } from '../../../common/data'
import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel'
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> {
interface Payload {
user_id: number | string
}
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString()
const uid = getUidByUin(user_id)
if (!uid) {
throw new Error('查无此人')
protected async _handle(payload: Payload): Promise<OB11User> {
if (!(getBuildVersion() >= 26702)) {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick,
sex: OB11UserSex.unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} else {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
}
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
}
}

View File

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

View File

@@ -1,18 +1,18 @@
import { OB11Group } from '../../types'
import { 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

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

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

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

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

@@ -2,38 +2,36 @@ import {
AtType,
ChatType,
ElementType,
Friend,
Group,
GroupMemberRole,
PicSubType,
RawMessage,
SendMessageElement,
} from '../../../ntqqapi/types'
import { friends, getGroup, getGroupMember, getUidByUin, selfInfo } 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 { ALLOW_SEND_TEMP_MSG, 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
@@ -45,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) {
@@ -67,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,
@@ -90,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
@@ -103,21 +131,28 @@ export async function createSendElements(
remainAtAllCount = (await NTQQGroupApi.getGroupAtAllRemainCount(groupCode)).atInfo
.RemainAtAllCountForUin
log(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await getGroupMember(groupCode, selfInfo.uin)
const self = await getGroupMember(groupCode, getSelfUin())
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner
} catch (e) {
}
}
if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'))
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '@全体成员'))
}
}
else if (peer.chatType === ChatType.group) {
// const atMember = group?.members.find(m => m.uin == atQQ)
const atMember = await getGroupMember(peer.peerUid, atQQ)
if (atMember) {
const display = `@${atMember.cardName || atMember.nick}`
sendElements.push(
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick),
SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, display),
)
} else {
const atNmae = sendMsg.data?.name
const uid = await NTQQUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : ''
sendElements.push(
SendMsgElementConstructor.at(atQQ, uid, AtType.atUser, display),
)
}
}
@@ -125,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(
@@ -159,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: {
@@ -277,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
}
@@ -294,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) {
@@ -307,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'
@@ -332,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!)
@@ -352,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',
@@ -423,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! }
}
@@ -452,12 +460,12 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const nodeMsg = await NTQQMsgApi.sendMsg(
{
chatType: ChatType.friend,
peerUid: selfInfo.uid,
peerUid: getSelfUid(),
},
sendElements,
true,
)
await sleep(500)
await sleep(400)
return nodeMsg
} catch (e) {
log(e, '克隆转发消息失败,将忽略本条消息', msg)
@@ -468,29 +476,21 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid,
peerUid: getSelfUid(),
}
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 !== selfInfo.uid) {
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 {
// 自定义的消息
@@ -524,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, () => {
@@ -536,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 !== selfInfo.uid) {
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之间用换行符分隔
@@ -578,13 +570,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空')
}
try {
log('开发转发', nodeMsgIds)
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
} catch (e) {
log('forward failed', e)
return null
}
const returnMsg = await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
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,13 +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 } from '@/ntqqapi/api'
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data'
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 {
@@ -62,16 +61,14 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc
}
async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
msg = msg as OB11Message
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11
let peer: Peer = {
const peer: Peer = {
chatType: ChatType.friend,
peerUid: msg.user_id.toString(),
}
if (msg.message_type == 'private') {
peer.peerUid = getUidByUin(msg.user_id.toString())!
peer.peerUid = (await NTQQUserApi.getUidByUin(msg.user_id.toString()))!
if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp
}
@@ -81,7 +78,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
peer.peerUid = msg.group_id?.toString()!
}
if (reply) {
let group: Group | null = null
let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) {
replyMessage.push({
@@ -93,7 +89,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
}
if (msg.message_type == 'group') {
group = (await getGroup(msg.group_id?.toString()!))!
if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({
type: 'at',
@@ -104,23 +99,26 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
}
}
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!)
log(`发送消息给`, peer, sendElements)
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, peer)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log)
}
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

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

View File

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

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

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

View File

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

View File

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

View File

@@ -17,19 +17,18 @@ import {
Group,
Peer,
GroupMember,
PicType,
RawMessage,
SelfInfo,
Sex,
TipGroupElementType,
User,
VideoElement,
FriendV2
FriendV2,
ChatType2
} from '../ntqqapi/types'
import { deleteGroup, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } 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'
@@ -59,15 +58,15 @@ export class OB11Constructor {
debug,
ob11: { messagePostFormat },
} = config
const message_type = msg.chatType == ChatType.group ? 'group' : 'private'
const selfUin = getSelfUin()
const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin),
self_id: parseInt(selfUin),
user_id: parseInt(msg.senderUin!),
time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId!,
real_id: msg.msgShortId!,
message_seq: msg.msgShortId!,
message_type: msg.chatType == ChatType.group ? 'group' : 'private',
message_type: msg.chatType === ChatType.group ? 'group' : 'private',
sender: {
user_id: parseInt(msg.senderUin!),
nickname: msg.sendNickName,
@@ -78,7 +77,7 @@ export class OB11Constructor {
sub_type: 'friend',
message: messagePostFormat === 'string' ? '' : [],
message_format: messagePostFormat === 'string' ? 'string' : 'array',
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
post_type: selfUin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
}
if (debug) {
resMsg.raw = msg
@@ -96,11 +95,15 @@ export class OB11Constructor {
resMsg.sub_type = 'friend'
resMsg.sender.nickname = (await NTQQUserApi.getUserDetailInfo(msg.senderUid)).nick
}
else if (msg.chatType == ChatType.temp) {
else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group'
const tempGroupCode = tempGroupCodeMap[msg.peerUin]
if (tempGroupCode) {
resMsg.group_id = parseInt(tempGroupCode)
const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid)
if (ret.result === 0) {
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
} else {
resMsg.group_id = 284840486 //兜底数据
resMsg.sender.nickname = '临时会话'
}
}
@@ -147,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'] = 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, msg.chatType)
// 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
@@ -328,7 +337,11 @@ export class OB11Constructor {
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
return new OB11FriendPokeEvent(
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
@@ -365,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)
@@ -413,24 +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()
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(selfInfo.uin),
parseInt(getSelfUin()),
parseInt(adminUin),
'kick_me',
'kick_me'
)
}
} catch (e) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave')
return new OB11GroupDecreaseEvent(
parseInt(msg.peerUid),
parseInt(getSelfUin()),
0,
'leave'
)
}
}
}
@@ -464,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 < 1) {
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)
}
@@ -533,7 +559,12 @@ export class OB11Constructor {
//筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail)
return new OB11GroupPokeEvent(
parseInt(msg.peerUid),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
}
}
if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
@@ -541,20 +572,23 @@ 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
const { msgList } = await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true)
//const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
//const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
if (msgList[0].senderUin === '0') {
msgList[0].senderUin = postMsg?.senderUin ?? selfInfo.uin
}
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), postMsg?.msgShortId!, parseInt(msgList[0].senderUin))
//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) {
@@ -575,27 +609,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)
}
}
@@ -665,7 +698,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

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

View File

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

View File

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

@@ -1,4 +1,4 @@
import { selfInfo } from '../../../common/data'
import { getSelfInfo } from '../../../common/data'
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent'
import { ActionName } from '../../action/types'
import { OB11Response } from '../../action/OB11Response'
@@ -78,6 +78,7 @@ export class ReverseWebsocket {
private connect() {
const { token, heartInterval } = getConfigUtil().getConfig()
const selfInfo = getSelfInfo()
this.websocket = new WebSocketClass(this.url, {
maxPayload: 1024 * 1024 * 1024,
handshakeTimeout: 2000,
@@ -103,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 { selfInfo } 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(() => {
postWsEvent(new OB11HeartbeatEvent(selfInfo.online!, true, heartInterval!))
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')),
@@ -411,10 +416,7 @@ async function onSettingWindowCreated(view: Element) {
buttonDom.addEventListener('click', async () => {
window.llonebot.checkVersion().then(checkVersionFunc)
})
return
}
if (!ResultVersion.result) {
} else if (!ResultVersion.result) {
titleDom.innerHTML = '当前已是最新版本 v' + version
buttonDom.innerHTML = '无需更新'
} else {

View File

@@ -1 +1 @@
export const version = '3.28.1'
export const version = '3.29.6'