Compare commits

...

31 Commits

Author SHA1 Message Date
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
60 changed files with 1334 additions and 1034 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -52,6 +52,7 @@ export class ConfigUtil {
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
msgCacheExpire: 120
} }
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {

View File

@@ -1,6 +1,5 @@
import { import {
type Friend, type Friend,
type Group,
type GroupMember, type GroupMember,
type SelfInfo, type SelfInfo,
} from '../ntqqapi/types' } from '../ntqqapi/types'
@@ -9,23 +8,25 @@ import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log' import { log } from './utils/log'
import { isNumeric } from './utils/helper' import { isNumeric } from './utils/helper'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api' import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
import { getBuildVersion } from './utils/QQBasicInfo'
export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
httpServerError: '', httpServerError: '',
wsServerError: '', wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误', otherError: 'LLOneBot 未能正常启动,请检查日志查看错误',
} }
// 群号 -> 群成员map(uid=>GroupMember) // 群号 -> 群成员map(uid=>GroupMember)
export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>() export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> { export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid' const filterKey: 'uin' | 'uid' = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
let filterValue = uinOrUid const filterValue = uinOrUid
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString()) let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend) { if (!friend && getBuildVersion() < 26702) {
try { try {
const _friends = await NTQQFriendApi.getFriends(true) const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString()) friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
@@ -39,39 +40,15 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
return friend return friend
} }
export async function getGroup(qq: string): Promise<Group | undefined> { export async function getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
let group = groups.find((group) => group.groupCode === qq.toString()) const groupCodeStr = groupCode.toString()
if (!group) { const memberUinOrUidStr = memberUinOrUid.toString()
try { let members = groupMembers.get(groupCodeStr)
const _groups = await NTQQGroupApi.getGroups(true)
group = _groups.find((group) => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
}
return group
}
export function deleteGroup(groupCode: string) {
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString())
// log(groups, groupCode, groupIndex);
if (groupIndex !== -1) {
log('删除群', groupCode)
groups.splice(groupIndex, 1)
}
}
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
let members = groupMembers.get(groupQQ)
if (!members) { if (!members) {
try { try {
members = await NTQQGroupApi.getGroupMembers(groupQQ) members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表 // 更新群成员列表
groupMembers.set(groupQQ, members) groupMembers.set(groupCodeStr, members)
} }
catch (e) { catch (e) {
return null return null
@@ -79,16 +56,17 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s
} }
const getMember = () => { const getMember = () => {
let member: GroupMember | undefined = undefined let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUid)) { if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUid) member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr)
} else { } else {
member = members!.get(memberUinOrUid) member = members!.get(memberUinOrUidStr)
} }
return member return member
} }
let member = getMember() let member = getMember()
if (!member) { if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupQQ) members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember() member = getMember()
} }
return member return member
@@ -102,7 +80,7 @@ const selfInfo: SelfInfo = {
} }
export async function getSelfNick(force = false): Promise<string> { export async function getSelfNick(force = false): Promise<string> {
if (!selfInfo.nick || force) { if ((!selfInfo.nick || force) && selfInfo.uid) {
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid) const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
if (userInfo) { if (userInfo) {
selfInfo.nick = userInfo.nick selfInfo.nick = userInfo.nick
@@ -127,4 +105,25 @@ export function getSelfUid() {
export function getSelfUin() { export function getSelfUin() {
return selfInfo['uin'] return selfInfo['uin']
}
const messages: Map<string, RawMessage> = new Map()
let expire: number
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
expire ??= getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) {
return
}
const id = msg.msgId
messages.set(id, msg)
setTimeout(() => {
messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
export function getMsgCache(msgId: string) {
return messages.get(msgId)
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import fs from 'node:fs'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { log, TEMP_DIR } from './index' import { log, TEMP_DIR } from './index'
import { dbUtil } from '../db'
import * as fileType from 'file-type' import * as fileType from 'file-type'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
@@ -187,13 +186,6 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
} else { } else {
filePath = pathname filePath = pathname
} }
} else {
const cache = await dbUtil.getFileCache(uri)
if (cache) {
filePath = cache.filePath
} else {
filePath = uri
}
} }
res.isLocal = true res.isLocal = true

View File

@@ -143,4 +143,27 @@ export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey:
} }
} }
return logExecutionTime return logExecutionTime
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr)
const low = BigInt(lowStr)
const highHex = high.toString(16).padStart(16, '0')
const lowHex = low.toString(16).padStart(16, '0')
const combinedHex = highHex + lowHex
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`
return uuid
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '')
const high = BigInt('0x' + hex.substring(0, 16))
const low = BigInt('0x' + hex.substring(16))
return { high: high.toString(), low: low.toString() }
}
} }

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5
export class LimitedHashTable<K, V> { export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map() private keyToValue: Map<K, V> = new Map()
private valueToKey: Map<V, K> = new Map() private valueToKey: Map<V, K> = new Map()

View File

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

View File

@@ -24,6 +24,7 @@ import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { NodeIKernelSearchService } from '@/ntqqapi/services'
export class NTQQFileApi { export class NTQQFileApi {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> { static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
@@ -178,7 +179,6 @@ export class NTQQFileApi {
const url: string = element.originImageUrl! // 没有域名 const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr const fileMd5 = element.md5HexStr
const fileUuid = element.fileUuid
if (url) { if (url) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
@@ -203,6 +203,122 @@ export class NTQQFileApi {
log('图片url获取失败', element) log('图片url获取失败', element)
return '' return ''
} }
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/core/src/apis/file.ts#L149
static async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) {
let GroupData: any[] | undefined
let BuddyData: any[] | undefined
if (peer.chatType === ChatType.group) {
GroupData =
[{
groupCode: peer.peerUid,
isConf: false,
hasModifyConfGroupFace: true,
hasModifyConfGroupName: true,
groupName: 'LLOneBot.Cached',
remark: 'LLOneBot.Cached',
}];
} else if (peer.chatType === ChatType.friend) {
BuddyData = [{
category_name: 'LLOneBot.Cached',
peerUid: peer.peerUid,
peerUin: peer.peerUid,
remark: 'LLOneBot.Cached',
}]
} else {
return undefined
}
const session = getSession()
return session?.getSearchService().addSearchHistory({
type: 4,
contactList: [],
id: -1,
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: peer.chatType,
buddyChatInfo: BuddyData || [],
discussChatInfo: [],
groupChatInfo: GroupData || [],
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: msgId,
msgSeq: msgSeq,
msgTime: Math.floor(Date.now() / 1000).toString(),
senderUid: senderUid,
senderNick: 'LLOneBot.Cached',
senderRemark: 'LLOneBot.Cached',
senderCard: 'LLOneBot.Cached',
elemId: elemId,
elemType: elemType,
fileSize: fileSize,
filePath: '',
fileName: fileName,
hits: [{
start: 12,
end: 14,
}],
},
],
})
}
static async searchfile(keys: string[]) {
type EventType = NodeIKernelSearchService['searchFileWithKeywords']
interface OnListener {
searchId: string,
hasMore: boolean,
resultItems: {
chatType: ChatType,
buddyChatInfo: any[],
discussChatInfo: any[],
groupChatInfo:
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}[],
dataLineChatInfo: any[],
tmpChatInfo: any[],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: number,
fileSize: string,
filePath: string,
fileName: string,
hits:
{
start: number,
end: number
}[]
}[]
}
const Event = NTEventDispatch.createEventFunction<EventType>('NodeIKernelSearchService/searchFileWithKeywords')
let id = ''
const Listener = NTEventDispatch.RegisterListen<(params: OnListener) => void>
(
'NodeIKernelSearchListener/onSearchFileKeywordsResult',
1,
20000,
(params) => id !== '' && params.searchId == id,
)
id = await Event!(keys, 12)
const [ret] = await Listener
return ret
}
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi {

View File

@@ -8,6 +8,7 @@ import { CacheClassFuncAsyncExtend } from '@/common/utils/helper'
import { LimitedHashTable } from '@/common/utils/table' import { LimitedHashTable } from '@/common/utils/table'
export class NTQQFriendApi { export class NTQQFriendApi {
/** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { data: {

View File

@@ -1,10 +1,11 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindowApi, NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners' import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services'
export class NTQQGroupApi { export class NTQQGroupApi {
static async activateMemberListChange() { static async activateMemberListChange() {
@@ -45,12 +46,29 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onGroupListUpdate', 'NodeIKernelGroupListener/onGroupListUpdate',
1, 1,
5000, 5000,
(updateType) => true, () => true,
forced forced
) )
return groupList 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>> { static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession() const session = getSession()
const groupService = session?.getGroupService() const groupService = session?.getGroupService()
@@ -100,6 +118,36 @@ export class NTQQGroupApi {
) )
} }
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) { static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|') const flagitem = flag.split('|')
const groupCode = flagitem[0] const groupCode = flagitem[0]

View File

@@ -208,8 +208,9 @@ export class NTQQMsgApi {
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> { static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const msgInfos = msgIds.map(async id => { const senderShowName = await getSelfNick()
return { msgId: id, senderShowName: await getSelfNick() } const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
}) })
const selfUid = getSelfUid() const selfUid = getSelfUid()
let data = await NTEventDispatch.CallNormalEvent< let data = await NTEventDispatch.CallNormalEvent<
@@ -269,4 +270,24 @@ export class NTQQMsgApi {
const session = getSession() 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

@@ -52,20 +52,11 @@ export class NTQQUserApi {
'NodeIKernelProfileListener/onUserDetailInfoChanged', 'NodeIKernelProfileListener/onUserDetailInfoChanged',
1, 1,
5000, 5000,
(profile) => { (profile) => profile.uid === uid,
if (profile.uid === uid) {
return true
}
return false
},
'BuddyProfileStore', 'BuddyProfileStore',
[ [uid],
uid
],
UserDetailSource.KSERVER, UserDetailSource.KSERVER,
[ [ProfileBizType.KALL]
ProfileBizType.KALL
]
) )
const RetUser: User = { const RetUser: User = {
...profile.simpleInfo.coreInfo, ...profile.simpleInfo.coreInfo,
@@ -81,7 +72,7 @@ export class NTQQUserApi {
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid) return NTQQUserApi.fetchUserDetailInfo(uid)
} }
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'] type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']
@@ -92,12 +83,7 @@ export class NTQQUserApi {
'NodeIKernelProfileListener/onProfileDetailInfoChanged', 'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2, 2,
5000, 5000,
(profile: User) => { (profile) => profile.uid === uid,
if (profile.uid === uid) {
return true
}
return false
},
uid, uid,
[0] [0]
) )
@@ -119,7 +105,7 @@ export class NTQQUserApi {
static async getQzoneCookies() { static async getQzoneCookies() {
const uin = getSelfUin() const uin = getSelfUin()
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + uin + '%2Finfocenter&keyindex=19%27' const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + (await NTQQUserApi.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string } = {} let cookies: { [key: string]: string } = {}
try { try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl) cookies = await RequestUtil.HttpsGetCookies(requestUrl)
@@ -129,8 +115,9 @@ export class NTQQUserApi {
} }
return cookies return cookies
} }
static async getSkey(): Promise<string> { static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey() const clientKeyData = await NTQQUserApi.getClientKey()
if (clientKeyData.result !== 0) { if (clientKeyData.result !== 0) {
throw new Error('获取clientKey失败') throw new Error('获取clientKey失败')
} }

View File

@@ -138,45 +138,47 @@ export class WebApi {
return ret return ret
} }
@CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members')
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
//logDebug('webapi 获取群成员', GroupCode) const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>() const cookieObject = await NTQQUserApi.getCookies('qun.qq.com')
try { const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com') const retList: Promise<WebApiGroupMemberRet>[] = []
const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') const params = new URLSearchParams({
const Bkn = WebApi.genBkn(CookiesObject.skey) st: '0',
const retList: Promise<WebApiGroupMemberRet>[] = [] end: '40',
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); sort: '1',
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { gc: GroupCode,
return [] bkn: WebApi.genBkn(cookieObject.skey)
} else { })
for (const key in fastRet.mems) { const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
MemberData.push(fastRet.mems[key]) if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
} return []
} else {
for (const member of fastRet.mems) {
memberData.push(member)
} }
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
retList.push(ret)
}
//批量等待
for (let i = 1; i <= PageNum; i++) {
const ret = await (retList[i])
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key])
}
}
} catch {
return MemberData
} }
return MemberData const pageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求
for (let i = 2; i <= pageNum; i++) {
params.set('st', String((i - 1) * 40))
params.set('end', String(i * 40))
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
retList.push(ret)
}
//批量等待
for (let i = 1; i <= pageNum; i++) {
const ret = await (retList[i])
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue
}
for (const member of ret.mems) {
memberData.push(member)
}
}
return memberData
} }
// public static async addGroupDigest(groupCode: string, msgSeq: string) { // public static async addGroupDigest(groupCode: string, msgSeq: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`; // const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`;
// const res = await this.request(url); // const res = await this.request(url);

View File

@@ -1,28 +1,30 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTQQMsgApi } from './api/msg' import { NTQQMsgApi } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types'
import { import {
deleteGroup, CategoryFriend,
ChatType,
GroupMember,
GroupMemberRole,
RawMessage,
SimpleInfo, User,
} from './types'
import {
friends, friends,
getFriend, getFriend,
getGroupMember, getGroupMember,
groups,
getSelfUin,
setSelfInfo setSelfInfo
} from '@/common/data' } from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config' import { getConfigUtil, HOOK_LOG } from '@/common/config'
import fs from 'fs' import fs from 'node:fs'
import { dbUtil } from '@/common/db'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique'
import { isNumeric, sleep } from '@/common/utils' import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent' import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent' import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import { randomUUID } from 'node:crypto'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -103,8 +105,8 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
if (hook.hookFunc.constructor.name === 'AsyncFunction') { if (hook.hookFunc.constructor.name === 'AsyncFunction') {
; (_ as Promise<void>).then() ; (_ as Promise<void>).then()
} }
} catch (e) { } catch (e: any) {
log('hook error', e, receiveData.payload) log('hook error', ntQQApiMethodName, e.stack.toString())
} }
}).then() }).then()
} }
@@ -233,16 +235,16 @@ export function removeReceiveHook(id: string) {
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1)
} }
let activatedGroups: string[] = [] //let activatedGroups: string[] = []
async function updateGroups(_groups: Group[], needUpdate: boolean = true) { /*async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) { for (let group of _groups) {
log('update group', group) log('update group', group.groupCode)
if (group.privilegeFlag === 0) { if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode) deleteGroup(group.groupCode)
continue continue
} }
log('update group', group) //log('update group', group)
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().catch(log) NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().catch(log)
let existGroup = groups.find((g) => g.groupCode == group.groupCode) let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) { if (existGroup) {
@@ -260,9 +262,9 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
} }
} }
} }
} }*/
async function processGroupEvent(payload: { groupList: Group[] }) { /*async function processGroupEvent(payload: { groupList: Group[] }) {
try { try {
const newGroupList = payload.groupList const newGroupList = payload.groupList
for (const group of newGroupList) { for (const group of newGroupList) {
@@ -313,12 +315,12 @@ async function processGroupEvent(payload: { groupList: Group[] }) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString()) log('更新群信息错误', e.stack.toString())
} }
} }*/
export async function startHook() { export async function startHook() {
// 群列表变动 // 群列表变动
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { /*registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
@@ -332,7 +334,7 @@ export async function startHook() {
}) })
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => { registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动 // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList) // log("群列表变动, store", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then()
} }
@@ -341,7 +343,7 @@ export async function startHook() {
processGroupEvent(payload).then() processGroupEvent(payload).then()
} }
} }
}) })*/
registerReceiveHook<{ registerReceiveHook<{
groupCode: string groupCode: string
@@ -391,17 +393,32 @@ export async function startHook() {
registerReceiveHook<{ registerReceiveHook<{
data: CategoryFriend[] data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => { }>(ReceiveCmdS.FRIENDS, (payload) => {
for (const fData of payload.data) { // log("onBuddyListChange", payload)
const _friends = fData.buddyList // let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
for (let friend of _friends) { type V2data = {userSimpleInfos: Map<string, SimpleInfo>}
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then() let friendList: User[] = [];
let existFriend = friends.find((f) => f.uin == friend.uin) if ((payload as any).userSimpleInfos) {
if (!existFriend) { // friendListV2 = payload as any
friends.push(friend) friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
} return {
else { ...v.coreInfo,
Object.assign(existFriend, friend)
} }
})
}
else{
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
}
}
log('好友列表变动', friendList)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
} }
} }
}) })
@@ -444,8 +461,12 @@ export async function startHook() {
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord const { msgId, chatType, peerUid } = msgRecord
dbUtil.addMsg(message).then() const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
}) })
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
@@ -508,4 +529,4 @@ export async function startHook() {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg }) log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
}) })
}) })
} }

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

@@ -7,4 +7,5 @@ export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService' export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService' export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelSearchService'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -219,6 +219,11 @@ async function onSettingWindowCreated(view: Element) {
`${window.LiteLoader.plugins['LLOneBot'].path.data}/logs`, `${window.LiteLoader.plugins['LLOneBot'].path.data}/logs`,
SettingButton('打开', 'config-open-log-path'), SettingButton('打开', 'config-open-log-path'),
), ),
SettingItem(
'消息内容缓存时长',
'单位为秒,可用于获取撤回的消息',
`<div class="q-input"><input class="q-input__inner" data-config-key="msgCacheExpire" type="number" min="1" value="${config.msgCacheExpire}" placeholder="${config.msgCacheExpire}" /></div>`,
),
]), ]),
SettingList([ SettingList([
SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')), SettingItem('GitHub 仓库', `https://github.com/LLOneBot/LLOneBot`, SettingButton('点个星星', 'open-github')),

View File

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