Compare commits

...

23 Commits

Author SHA1 Message Date
idranme
ac07c98ae1 Merge pull request #434 from LLOneBot/dev
release: 3.33.2
2024-09-20 23:00:09 +08:00
idranme
6a19d6f234 chore: v3.33.2 2024-09-20 22:57:00 +08:00
idranme
ab0b8ae663 feat: get_essence_msg_list API 2024-09-20 22:55:29 +08:00
idranme
96aa5e264a refactor 2024-09-20 22:13:26 +08:00
idranme
15b85f735d fix: friend_add event 2024-09-20 19:08:22 +08:00
idranme
4dd6d12168 feat 2024-09-20 18:00:32 +08:00
idranme
44febed486 optimize 2024-09-20 03:19:43 +08:00
idranme
6c66dab3dc Merge pull request #433 from LLOneBot/dev
release: 3.33.1
2024-09-19 18:31:01 +08:00
idranme
0f7939fe5e chore: v3.33.1 2024-09-19 18:29:16 +08:00
idranme
73a2b4e35f fix 2024-09-19 18:29:12 +08:00
idranme
936b1d911c Merge pull request #428 from LLOneBot/dev
release: 3.33.0
2024-09-18 20:59:57 +08:00
idranme
58817d1c02 chore: v3.33.0 2024-09-18 20:53:28 +08:00
idranme
2c24422478 feat: support setting remark when agreeing to a friend request 2024-09-18 20:47:45 +08:00
idranme
c2a723380a fix: get_group_member_list API 2024-09-18 19:35:58 +08:00
idranme
156bbaea33 feat: get_group_files_by_folder API 2024-09-18 17:22:09 +08:00
idranme
6c485634e1 feat: get_friend_msg_history API 2024-09-18 16:56:15 +08:00
idranme
f39a9aeafb feat: fetch_custom_face API 2024-09-18 16:11:08 +08:00
idranme
1160cd4b26 feat: fetch_emoji_like API 2024-09-18 15:49:37 +08:00
idranme
9a7ff523dd optimize 2024-09-18 14:07:42 +08:00
idranme
f49995ea97 refactor 2024-09-17 21:04:36 +08:00
idranme
1876dd29ac Merge pull request #423 from LLOneBot/dev
release: 3.32.8
2024-09-17 11:59:57 +08:00
idranme
9944b53266 chore: v3.32.8 2024-09-17 11:55:50 +08:00
idranme
9a791e3a21 fix 2024-09-17 02:17:16 +08:00
64 changed files with 999 additions and 1111 deletions

7
.gitattributes vendored Normal file
View File

@@ -0,0 +1,7 @@
* text eol=lf
*.png -text
*.jpg -text
*.ico -text
*.gif -text
*.webp -text

View File

@@ -31,7 +31,6 @@ const config: ElectronViteConfig = {
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
},
},
plugins: [

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用于 QQ 机器人开发",
"version": "3.32.7",
"version": "3.33.2",
"icon": "./icon.webp",
"authors": [
{

View File

@@ -17,16 +17,16 @@
"author": "",
"license": "MIT",
"dependencies": {
"@minatojs/driver-sqlite": "^4.5.0",
"@minatojs/driver-sqlite": "^4.6.0",
"compressing": "^1.10.1",
"cordis": "^3.18.0",
"cordis": "^3.18.1",
"cors": "^2.8.5",
"cosmokit": "^1.6.2",
"express": "^5.0.0",
"fast-xml-parser": "^4.5.0",
"file-type": "^19.5.0",
"fluent-ffmpeg": "^2.1.3",
"minato": "^3.5.1",
"minato": "^3.6.0",
"protobufjs": "^7.4.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
@@ -41,7 +41,7 @@
"electron-vite": "^2.3.0",
"protobufjs-cli": "^1.1.3",
"typescript": "^5.6.2",
"vite": "^5.4.5",
"vite": "^5.4.6",
"vite-plugin-cp": "^4.0.8"
},
"packageManager": "yarn@4.4.1"

View File

@@ -91,17 +91,26 @@ interface FetchFileRes {
}
export async function fetchFile(url: string, headersInit?: Record<string, string>): Promise<FetchFileRes> {
const headers: Record<string, string> = {
const headers = new Headers({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
'Host': new URL(url).hostname,
...headersInit
}
const raw = await fetch(url, { headers }).catch((err) => {
})
let raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
if (raw.status === 403 && !headers.has('Referer')) {
headers.set('Referer', url)
raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
}
if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`)
return {
data: Buffer.from(await raw.arrayBuffer()),
@@ -133,7 +142,7 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
if (type === FileUriType.RemoteURL) {
try {
const res = await fetchFile(uri, { 'Referer': uri })
const res = await fetchFile(uri)
const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
if (match?.[1]) {
filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_')

View File

@@ -1,163 +0,0 @@
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import Database, { Tables } from 'minato'
import SQLite from '@minatojs/driver-sqlite'
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from './table'
import { DATA_DIR } from '../globalVars'
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 {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> {
const data = this.msgDataMap.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId)
}
async getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId)
if (!shortId) return undefined
return await this.getMsgIdAndPeerByShortId(shortId)
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize)
this.msgDataMap.resize(maxSize)
}
addFileCache(data: FileCacheV2) {
return this.db?.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.db?.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.db?.get('file_v2', { fileUuid })
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@@ -1,5 +1,4 @@
import path from 'node:path'
import fs from 'node:fs'
import Log from './log'
import Core from '../ntqqapi/core'
import OneBot11Adapter from '../onebot11/adapter'
@@ -34,6 +33,11 @@ import {
NTQQWebApi,
NTQQWindowApi
} from '../ntqqapi/api'
import { mkdir } from 'node:fs/promises'
import { existsSync, mkdirSync } from 'node:fs'
import Database from 'minato'
import SQLiteDriver from '@minatojs/driver-sqlite'
import Store from './store'
declare module 'cordis' {
interface Events {
@@ -45,12 +49,12 @@ let mainWindow: BrowserWindow | null = null
// 加载插件时触发
function onLoad() {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true })
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true })
}
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR)
if (!existsSync(LOG_DIR)) {
mkdirSync(LOG_DIR)
}
ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
@@ -151,8 +155,12 @@ function onLoad() {
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR)
if (!existsSync(TEMP_DIR)) {
await mkdir(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
await mkdir(dbDir)
}
const ctx = new Context()
ctx.plugin(Log, {
@@ -179,6 +187,11 @@ function onLoad() {
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
ctx.plugin(Database)
ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`)
})
ctx.plugin(Store)
ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config)

126
src/main/store.ts Normal file
View File

@@ -0,0 +1,126 @@
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from '@/common/utils/table'
import { FileCacheV2 } from '@/common/types'
import { Context, Service } from 'cordis'
declare module 'cordis' {
interface Context {
store: Store
}
interface Tables {
message: {
shortId: number
msgId: string
chatType: number
peerUid: string
}
file_v2: FileCacheV2
}
}
interface MsgInfo {
msgId: string
peer: Peer
}
export default class Store extends Service {
static inject = ['database', 'model']
private cache: LimitedHashTable<string, number>
constructor(protected ctx: Context) {
super(ctx, 'store', true)
this.cache = new LimitedHashTable<string, number>(1000)
this.initDatabase()
}
private async initDatabase() {
this.ctx.model.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
this.ctx.model.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']
})
}
createMsgShortId(peer: Peer, msgId: string): number {
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(cacheKey).digest()
hash[0] &= 0x7f //设置第一个bit为0 保证shortId为正数
const shortId = hash.readInt32BE()
this.cache.set(cacheKey, shortId)
this.ctx.database.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgInfoByShortId(shortId: number): Promise<MsgInfo | undefined> {
const data = this.cache.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
return {
msgId,
peer: {
chatType: +chatTypeStr,
peerUid,
guildId: ''
}
}
}
const items = await this.ctx.database.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
msgId,
peer: {
chatType,
peerUid,
guildId: ''
}
}
}
}
async getShortIdByMsgId(msgId: string): Promise<number | undefined> {
return (await this.ctx.database.get('message', { msgId }))[0]?.shortId
}
getShortIdByMsgInfo(peer: Peer, msgId: string) {
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
return this.cache.getValue(cacheKey)
}
addFileCache(data: FileCacheV2) {
return this.ctx.database.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.ctx.database.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.ctx.database.get('file_v2', { fileUuid })
}
}

View File

@@ -13,15 +13,14 @@ import {
PicElement,
} from '../types'
import path from 'node:path'
import fs from 'node:fs'
import { existsSync } from 'node:fs'
import { ReceiveCmdS } from '../hook'
import { RkeyManager } from '@/ntqqapi/helper/rkey'
import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { copyFile, stat, unlink } from 'node:fs/promises'
import { Time } from 'cosmokit'
import { Service, Context } from 'cordis'
import { TEMP_DIR } from '@/common/globalVars'
@@ -76,18 +75,13 @@ export class NTQQFileApi extends Service {
// 上传文件到QQ的文件夹
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath)
let ext = (await this.getFileType(filePath))?.ext || ''
if (ext) {
ext = '.' + ext
let fileName = path.basename(filePath)
if (!fileName.includes('.')) {
const ext = (await this.getFileType(filePath))?.ext
fileName += ext ? '.' + ext : ''
}
let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) {
fileName += ext
}
const session = getSession()
let mediaPath: string
if (session) {
mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
const mediaPath = await invoke(NTMethod.MEDIA_FILE_PATH, [{
path_info: {
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
@@ -95,30 +89,16 @@ export class NTQQFileApi extends Service {
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
})
} else {
mediaPath = await invoke(NTMethod.MEDIA_FILE_PATH, [{
path_info: {
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
}])
}
await fsPromise.copyFile(filePath, mediaPath)
const fileSize = (await fsPromise.stat(filePath)).size
file_uuid: '',
},
}])
await copyFile(filePath, mediaPath)
const fileSize = (await stat(filePath)).size
return {
md5: fileMd5,
fileName,
path: mediaPath,
fileSize,
ext
}
}
@@ -133,10 +113,10 @@ export class NTQQFileApi extends Service {
force = false
) {
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) {
if (sourcePath && existsSync(sourcePath)) {
if (force) {
try {
await fsPromise.unlink(sourcePath)
await unlink(sourcePath)
} catch { }
} else {
return sourcePath
@@ -177,7 +157,11 @@ export class NTQQFileApi extends Service {
}
async getImageSize(filePath: string) {
return await invoke<{ width: number; height: number }>(
return await invoke<{
width: number
height: number
type: string
}>(
NTMethod.IMAGE_SIZE,
[filePath],
{

View File

@@ -42,13 +42,7 @@ export class NTQQFriendApi extends Service {
return _friends
}
async handleFriendRequest(flag: string, accept: boolean) {
const data = flag.split('|')
if (data.length < 2) {
return
}
const friendUid = data[0]
const reqTime = data[1]
async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
const session = getSession()
if (session) {
return session.getBuddyService().approvalFriendRequest({
@@ -194,4 +188,10 @@ export class NTQQFriendApi extends Service {
const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }, null])
return ret.arkMsg
}
async setBuddyRemark(uid: string, remark: string) {
return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{
remarkParams: { uid, remark }
}, null])
}
}

View File

@@ -1,10 +1,18 @@
import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GetFileListParam, PublishGroupBulletinReq } from '../types'
import {
Group,
GroupMember,
GroupMemberRole,
GroupNotifies,
GroupRequestOperateTypes,
GetFileListParam,
OnGroupFileInfoUpdateParams,
PublishGroupBulletinReq
} from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window'
import { getSession } from '../wrapper'
import { OnGroupFileInfoUpdateParams } from '../listeners'
import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc'
@@ -225,34 +233,48 @@ export class NTQQGroupApi extends Service {
>(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }, null])
}
/** 27187 TODO */
async removeGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
const data = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const param = {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().removeGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param)
}
/** 27187 TODO */
async addGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
const data = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
const param = {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().addGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/addGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
}
async createGroupFileFolder(groupId: string, folderName: string) {
@@ -303,4 +325,14 @@ export class NTQQGroupApi extends Service {
const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }, null])
return ret.arkJson
}
async queryCachedEssenceMsg(groupCode: string, msgSeq = '0', msgRandom = '0') {
return await invoke('nodeIKernelGroupService/queryCachedEssenceMsg', [{
key: {
groupCode,
msgSeq: +msgSeq,
msgRandom: +msgRandom
}
}, null])
}
}

View File

@@ -235,7 +235,7 @@ export class NTQQMsgApi extends Service {
}, null])
}
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[]) {
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
@@ -256,4 +256,23 @@ export class NTQQMsgApi extends Service {
async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }, null])
}
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
return await invoke('nodeIKernelMsgService/getMsgEmojiLikesList', [{
peer,
msgSeq,
emojiId,
emojiType,
cnt: count
}, null])
}
async fetchFavEmojiList(count: number) {
return await invoke('nodeIKernelMsgService/fetchFavEmojiList', [{
resId: '',
count,
backwardFetch: true,
forceRefresh: true
}, null])
}
}

View File

@@ -102,18 +102,6 @@ export class NTQQUserApi extends Service {
return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null])
}
genBkn(sKey: string) {
sKey = sKey || ''
let hash = 5381
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i)
hash = hash + (hash << 5) + code
}
return (hash & 0x7fffffff).toString()
}
async like(uid: string, count = 1) {
const session = getSession()
if (session) {

View File

@@ -50,55 +50,6 @@ interface WebApiGroupMemberRet {
extmode: number
}
interface GroupEssenceMsg {
group_code: string
msg_seq: number
msg_random: number
sender_uin: string
sender_nick: string
sender_time: number
add_digest_uin: string
add_digest_nick: string
add_digest_time: number
msg_content: unknown[]
can_be_removed: true
}
export interface GroupEssenceMsgRet {
retcode: number
retmsg: string
data: {
msg_list: GroupEssenceMsg[]
is_end: boolean
group_role: number
config_page_url: string
}
}
interface SetGroupNoticeParams {
groupCode: string
content: string
pinned: number
type: number
isShowEditCard: number
tipWindowType: number
confirmRequired: number
picId: string
imgWidth?: number
imgHeight?: number
}
interface SetGroupNoticeRet {
ec: number
em: string
id: number
ltsm: number
new_fid: string
read_only: number
role: number
srv_code: number
}
export class NTQQWebApi extends Service {
static inject = ['ntUserApi']
@@ -274,34 +225,6 @@ export class NTQQWebApi extends Service {
return honorInfo
}
async setGroupNotice(params: SetGroupNoticeParams): Promise<SetGroupNoticeRet> {
const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const settings = JSON.stringify({
is_show_edit_card: params.isShowEditCard,
tip_window_type: params.tipWindowType,
confirm_required: params.confirmRequired
})
return await RequestUtil.HttpGetJson<SetGroupNoticeRet>(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.genBkn(cookieObject.skey),
qid: params.groupCode,
text: params.content,
pinned: params.pinned.toString(),
type: params.type.toString(),
settings: settings,
...(params.picId !== '' && {
pic: params.picId,
imgWidth: params.imgWidth?.toString(),
imgHeight: params.imgHeight?.toString(),
})
})}`,
'POST',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
)
}
private cookieToString(cookieObject: Dict) {
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
}

View File

@@ -1,7 +1,6 @@
import fs from 'node:fs'
import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { MessageUnique } from '../common/utils/messageUnique'
import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc'
@@ -45,7 +44,6 @@ class Core extends Service {
public start() {
llonebotError.otherError = ''
MessageUnique.init(selfInfo.uin)
this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => {

View File

@@ -16,7 +16,7 @@ import {
SendVideoElement,
} from './types'
import { stat, writeFile, copyFile, unlink } from 'node:fs/promises'
import { calculateFileMD5, isGIF } from '../common/utils/file'
import { calculateFileMD5 } from '../common/utils/file'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio'
import { Context } from 'cordis'
@@ -66,14 +66,10 @@ export namespace SendElementEntities {
}
}
export async function pic(ctx: Context, picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> {
export async function pic(ctx: Context, picPath: string, summary = '', subType: 0 | 1 = 0, isFlashPic?: boolean): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(picPath, ElementType.PIC, subType)
if (fileSize === 0) {
throw '文件异常大小为0'
}
const maxMB = 30;
if (fileSize > 1024 * 1024 * 30) {
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
throw '文件异常,大小为 0'
}
const imageSize = await ctx.ntFileApi.getImageSize(picPath)
const picElement = {
@@ -84,12 +80,13 @@ export namespace SendElementEntities {
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picType: imageSize.type === 'gif' ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary,
isFlashPic,
}
ctx.logger.info('图片信息', picElement)
return {
@@ -365,4 +362,16 @@ export namespace SendElementEntities {
},
}
}
export function shake(): SendFaceElement {
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: 1,
faceType: 5,
pokeType: 1,
},
}
}
}

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
import { User, UserDetailInfoListenerArg } from '@/ntqqapi/types'
export interface IProfileListener {
onProfileSimpleChanged(...args: unknown[]): void
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void
onProfileDetailInfoChanged(profile: User): void
onStatusUpdate(...args: unknown[]): void
onSelfStatusChanged(...args: unknown[]): void
onStrangerRemarkChanged(...args: unknown[]): void
}

View File

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

View File

@@ -123,11 +123,15 @@ export function invoke<
return new Promise<R>((resolve, reject) => {
const apiArgs = [method, ...args]
const callbackId = randomUUID()
let success = false
const timeoutId = setTimeout(() => {
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
}, timeout)
if (!options.cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[callbackId] = res => {
success = true
clearTimeout(timeoutId)
resolve(res)
}
}
@@ -139,13 +143,13 @@ export function invoke<
if (options.cmdCB) {
if (options.cmdCB(payload, result)) {
removeReceiveHook(hookId)
success = true
clearTimeout(timeoutId)
resolve(payload)
}
}
else {
removeReceiveHook(hookId)
success = true
clearTimeout(timeoutId)
resolve(payload)
}
})
@@ -158,16 +162,11 @@ export function invoke<
}
else {
log('ntqq api call failed,', method, res)
clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`)
}
}
}
setTimeout(() => {
if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
}
}, timeout)
ipcMain.emit(
channel,

View File

@@ -72,14 +72,24 @@ export interface NodeIKernelGroupService {
}
}): Promise<unknown>
//26702(其实更早 但是我不知道)
isEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
isEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
queryCachedEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
queryCachedEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<{
items: {
groupCode: string
msgSeq: number
msgRandom: number
msgSenderUin: string
msgSenderNick: string
opType: number
opUin: string
opNick: string
opTime: number
grayTipSeq: string
}[]
}>
//26702(其实更早 但是我不知道)
fetchGroupEssenceList(Req: { groupCode: string, pageStart: number, pageLimit: number }, Arg: unknown): Promise<unknown>
fetchGroupEssenceList(req: { groupCode: string, pageStart: number, pageLimit: number }, arg: unknown): Promise<unknown>
//26702
getAllMemberList(groupCode: string, forceFetch: boolean): Promise<{
@@ -104,11 +114,20 @@ export interface NodeIKernelGroupService {
createMemberListScene(groupCode: string, scene: string): string
destroyMemberListScene(SceneId: string): void
//About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string}
destroyMemberListScene(sceneId: string): void
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number, errMsg: string,
result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean }
errCode: number
errMsg: string
result: {
ids: {
uid: string
index: number
}[]
infos: Map<string, GroupMember>
finish: boolean
hasRobot: boolean
}
}>
getPrevMemberList(): unknown

View File

@@ -214,7 +214,7 @@ export interface NodeIKernelMsgService {
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
//cnt clientSeq?并不是吧
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown
@@ -223,31 +223,8 @@ export interface NodeIKernelMsgService {
queryMsgsWithFilter(...args: unknown[]): unknown
/**
* @deprecated 该函数已被标记为废弃,请使用新的替代方法。
* 使用过滤条件查询消息列表的版本2接口。
*
* 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
* 函数返回一个Promise解析为查询结果的未知类型对象。
*
* @param MsgId 消息ID用于特定消息的查询。
* @param MsgTime 消息时间,用于指定消息的时间范围。
* @param param 查询参数对象,包含详细的过滤条件和分页信息。
* @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
* @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
* @param param.filterSendersUid 需要过滤的发送者用户ID数组。
* @param param.filterMsgFromTime 查询消息的起始时间。
* @param param.filterMsgToTime 查询消息的结束时间。
* @param param.pageLimit 每页的消息数量限制。
* @param param.isReverseOrder 是否按时间顺序倒序返回消息。
* @param param.isIncludeCurrent 是否包含当前页码。
* @returns 返回一个Promise解析为查询结果的未知类型对象。
*/
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>
// this.chatType = i2
// this.peerUid = str
// this.chatInfo = new ChatInfo()
// this.filterMsgType = new ArrayList<>()
// this.filterSendersUid = new ArrayList<>()
@@ -495,16 +472,15 @@ export interface NodeIKernelMsgService {
setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number,
errMsg: string,
emojiLikesList:
Array<{
tinyId: string,
nickName: string,
result: number
errMsg: string
emojiLikesList: {
tinyId: string
nickName: string
headUrl: string
}>,
cookie: string,
isLastPage: boolean,
}[]
cookie: string
isLastPage: boolean
isFirstPage: boolean
}>
@@ -686,9 +662,8 @@ export interface NodeIKernelMsgService {
dataMigrationStopOperation(...args: unknown[]): unknown
//新的希望
dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{
extensionData: string//"Hex"
extensionData: string //"Hex"
extraData: string //""
chatType: number
chatUin: string
@@ -740,4 +715,4 @@ export interface NodeIKernelMsgService {
getGroupMsgStorageTime(): unknown//这是嘛啊
}
}

View File

@@ -62,8 +62,9 @@ export interface GroupMember {
sex?: Sex
qqLevel?: QQLevel
isChangeRole: boolean
joinTime: string
lastSpeakTime: string
joinTime: number
lastSpeakTime: number
memberLevel: number
}
export interface PublishGroupBulletinReq {
@@ -76,4 +77,4 @@ export interface PublishGroupBulletinReq {
oldFeedsId: ''
pinned: number
confirmRequired: number
}
}

View File

@@ -6,6 +6,7 @@ export interface GetFileListParam {
startIndex: number
sortOrder: number
showOnlinedocFolder: number
folderId?: string
}
export enum ElementType {
@@ -348,6 +349,7 @@ export interface FaceElement {
resultId?: string
surpriseId?: string
randomType?: number
pokeType?: number
}
export interface MarketFaceElement {
@@ -481,7 +483,7 @@ export interface RawMessage {
msgSeq: string
msgRandom: string
senderUid: string
senderUin?: string // 发送者QQ号
senderUin: string // 发送者QQ号
peerUid: string // 群号 或者 QQ uid
peerUin: string // 群号 或者 发送者QQ号
guildId: string
@@ -533,3 +535,77 @@ export interface MessageElement {
recommendedMsgElement?: unknown
actionBarElement?: unknown
}
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string
msgElementId: string
msgId: string
fileId: string
fileProgress: string // '0'
fileSpeed: string // '0'
fileErrCode: string // '0'
fileErrMsg: string
fileDownType: number // 暂时未知
thumbSize: number
filePath: string
totalSize: string
trasferStatus: number
step: number
commonFileInfo: unknown
fileSrvErrCode: string
clientMsg: string
businessId: number
userTotalSpacePerDay: unknown
userUsedSpacePerDay: unknown
}
export interface OnGroupFileInfoUpdateParams {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: {
peerId: string
type: number
folderInfo?: {
folderId: string
parentFolderId: string
folderName: string
createTime: number
modifyTime: number
createUin: string
creatorName: string
totalFileCount: number
modifyUin: string
modifyName: string
usedSpace: string
}
fileInfo?: {
fileModelId: string
fileId: string
fileName: string
fileSize: string
busId: number
uploadedSize: string
uploadTime: number
deadTime: number
modifyTime: number
downloadTimes: number
sha: string
sha3: string
md5: string
uploaderLocalPath: string
uploaderName: string
uploaderUin: string
parentFolderId: string
localPath: string
transStatus: number
transType: number
elementId: string
isFolder: boolean
}
}[]
allFileCount: number
nextIndex: number
reqId: number
}

View File

@@ -33,30 +33,8 @@ export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
export interface WrapperConstructor {
[key: string]: unknown
}
const wrapperApi: WrapperApi = {}
export const wrapperConstructor: WrapperConstructor = {}
const constructor = [
'NodeIKernelBuddyListener',
'NodeIKernelGroupListener',
'NodeQQNTWrapperUtil',
'NodeIKernelMsgListener',
'NodeIQQNTWrapperEngine',
'NodeIGlobalAdapter',
'NodeIDependsAdapter',
'NodeIDispatcherAdapter',
'NodeIKernelSessionListener',
'NodeIKernelLoginService',
'NodeIKernelLoginListener',
'NodeIKernelProfileService',
'NodeIKernelProfileListener',
]
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
@@ -69,9 +47,6 @@ Process.dlopen = function (module: Dict, filename: string, flags = constants.dlo
return ret
}
})
if (constructor.includes(export_name)) {
wrapperConstructor[export_name] = module.exports[export_name]
}
}
return dlopenRet
}

View File

@@ -2,7 +2,6 @@ import { BaseAction, Schema } from '../BaseAction'
import { readFile } from 'node:fs/promises'
import { ActionName } from '../types'
import { Peer, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique'
export interface GetFilePayload {
file: string // 文件名或者fileUuid
@@ -24,9 +23,9 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const { enableLocalFile2Url } = this.adapter.config
let fileCache = await MessageUnique.getFileCacheById(payload.file)
let fileCache = await this.ctx.store.getFileCacheById(payload.file)
if (!fileCache?.length) {
fileCache = await MessageUnique.getFileCacheByName(payload.file)
fileCache = await this.ctx.store.getFileCacheByName(payload.file)
}
if (fileCache?.length) {

View File

@@ -1,6 +1,5 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
@@ -13,13 +12,13 @@ export class DelEssenceMsg extends BaseAction<Payload, unknown> {
})
protected async _handle(payload: Payload) {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
return await this.ctx.ntGroupApi.removeGroupEssence(
msg.Peer.peerUid,
msg.MsgId,
msg.peer.peerUid,
msg.msgId,
)
}
}

View File

@@ -1,8 +1,7 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { OB11ForwardMessage } from '../../types'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
import { filterNullable } from '@/common/utils/misc'
interface Payload {
@@ -16,17 +15,22 @@ interface Response {
export class GetForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg
payloadSchema = Schema.object({
message_id: String,
id: String
})
protected async _handle(payload: Payload) {
const msgId = payload.id || payload.message_id
if (!msgId) {
throw Error('message_id不能为空')
}
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId)
const rootMsg = await MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || +msgId)
const rootMsgId = await this.ctx.store.getShortIdByMsgId(msgId)
const rootMsg = await this.ctx.store.getMsgInfoByShortId(rootMsgId || +msgId)
if (!rootMsg) {
throw Error('msg not found')
}
const data = await this.ctx.ntMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId)
const data = await this.ctx.ntMsgApi.getMultiMsg(rootMsg.peer, rootMsg.msgId, rootMsg.msgId)
if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg)
}
@@ -35,7 +39,7 @@ export class GetForwardMsg extends BaseAction<Payload, Response> {
msgList.map(async (msg) => {
const resMsg = await OB11Entities.message(this.ctx, msg)
if (!resMsg) return
resMsg.message_id = MessageUnique.createMsg({
resMsg.message_id = this.ctx.store.createMsgShortId({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId)

View File

@@ -1,4 +1,4 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
@@ -13,6 +13,9 @@ interface Response {
export class GetGroupAtAllRemain extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupRemainAtTimes(payload.group_id.toString())

View File

@@ -0,0 +1,54 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types'
interface Payload {
group_id: string | number
folder_id: string
file_count: string | number
}
interface Response {
files: OB11GroupFile[]
folders: OB11GroupFileFolder[]
}
export class GetGroupFilesByFolder extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
folder_id: Schema.string().required(),
file_count: Schema.union([Number, String]).default(50)
})
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId: payload.folder_id
})
return {
files: data.filter(item => item.fileInfo)
.map(item => {
const file = item.fileInfo!
return {
group_id: +item.peerId,
file_id: file.fileId,
file_name: file.fileName,
busid: file.busId,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,
download_times: file.downloadTimes,
uploader: +file.uploaderUin,
uploader_name: file.uploaderName
}
}),
folders: []
}
}
}

View File

@@ -1,17 +1,16 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { OB11Message } from '../../types'
import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types'
import { OB11Entities } from '../../entities'
import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique'
import { filterNullable } from '@/common/utils/misc'
interface Payload {
group_id: number | string
message_seq?: number | string
count?: number | string
reverseOrder?: boolean
count: number | string
reverseOrder: boolean
}
interface Response {
@@ -20,25 +19,30 @@ interface Response {
export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
message_seq: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false),
})
protected async _handle(payload: Payload): Promise<Response> {
const count = payload.count || 20
const isReverseOrder = payload.reverseOrder || true
const { count, reverseOrder } = payload
const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[] | undefined
// 包含 message_seq 0
if (!payload.message_seq) {
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq))?.MsgId
const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
}
if (!msgList?.length) throw new Error('未找到消息')
if (isReverseOrder) msgList.reverse()
if (reverseOrder) msgList.reverse()
await Promise.all(
msgList.map(async msg => {
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
msg.msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
})
)
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Entities.message(this.ctx, msg)))

View File

@@ -1,4 +1,4 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
@@ -14,18 +14,19 @@ interface Response {
export class GetGroupRootFiles extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupRootFiles
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_count: Schema.union([Number, String]).default(50),
})
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +(payload.file_count ?? 50),
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
this.ctx.logger.info(data)
return {
files: data.filter(item => item.fileInfo)
.map(item => {

View File

@@ -1,4 +1,4 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { OB11User } from '../../types'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
@@ -12,6 +12,9 @@ interface Payload {
export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload): Promise<OB11User> {
if (!(getBuildVersion() >= 26702)) {

View File

@@ -1,6 +1,5 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
@@ -8,16 +7,16 @@ interface Payload {
export class MarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead
payloadSchema = Schema.object({
message_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
if (!payload.message_id) {
throw new Error('参数 message_id 不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
await this.ctx.ntMsgApi.setMsgRead(msg.Peer)
await this.ctx.ntMsgApi.setMsgRead(msg.peer)
return null
}
}

View File

@@ -9,6 +9,7 @@ interface Payload {
export class HandleQuickOperation extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_HandleQuickOperation
protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(this.ctx, payload.context, payload.operation).catch(e => this.ctx.logger.error(e))
return null

View File

@@ -1,20 +1,181 @@
import SendMsg from '../msg/SendMsg'
import { OB11PostSendMsg } from '../../types'
import { unlink } from 'node:fs/promises'
import { OB11MessageNode } from '../../types'
import { ActionName } from '../types'
import { BaseAction, Schema } from '../BaseAction'
import { Peer } from '@/ntqqapi/types/msg'
import { ChatType, ElementType, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
export class SendForwardMsg extends SendMsg {
interface Payload {
user_id?: string | number
group_id?: string | number
messages: OB11MessageNode[]
message_type?: 'group' | 'private'
}
interface Response {
message_id: number
forward_id?: string
}
export class SendForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_SendForwardMsg
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]),
group_id: Schema.union([Number, String]),
messages: Schema.array(Schema.any()).required(),
message_type: Schema.union(['group', 'private'])
})
protected async _handle(payload: OB11PostSendMsg) {
if (payload.messages) payload.message = payload.messages
return super._handle(payload)
protected async _handle(payload: Payload) {
let contextMode = CreatePeerMode.Normal
if (payload.message_type === 'group') {
contextMode = CreatePeerMode.Group
} else if (payload.message_type === 'private') {
contextMode = CreatePeerMode.Private
}
const peer = await createPeer(this.ctx, payload, contextMode)
const returnMsg = await this.handleForwardNode(peer, payload.messages)
return { message_id: returnMsg.msgShortId! }
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
this.ctx.logger.info('克隆的目标消息', msg)
const sendElements: SendMessageElement[] = []
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement)
}
if (sendElements.length === 0) {
this.ctx.logger.warn('需要clone的消息无法解析将会忽略掉', msg)
}
this.ctx.logger.info('克隆消息', sendElements)
try {
const peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
const nodeMsg = await this.ctx.ntMsgApi.sendMsg(peer, sendElements)
await this.ctx.sleep(300)
return nodeMsg
} catch (e) {
this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg)
}
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid,
}
const nodeMsgIds: { msgId: string, peer: Peer }[] = []
// 先判断一遍是不是id和自定义混用
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
const nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
const nodeMsg = await this.ctx.store.getMsgInfoByShortId(+nodeId)
if (!nodeMsg) {
this.ctx.logger.warn('转发消息失败,未找到消息', nodeId)
continue
}
nodeMsgIds.push(nodeMsg)
}
else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx,
convertMessage2List(messageNode.data.content),
destPeer
)
this.ctx.logger.info('开始生成转发节点', sendElements)
const sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0
for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = []
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++
}
else {
sendElementsSplit[splitIndex].push(ele)
}
}
this.ctx.logger.info('分割后的转发节点', sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [])
if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles)
continue
}
nodeMsgIds.push({ msgId: nodeMsg.msgId, peer: selfPeer })
await this.ctx.sleep(300)
}
deleteAfterSentFiles.map(path => unlink(path))
} catch (e) {
this.ctx.logger.error('生成转发消息节点失败', e)
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer
let needSendSelf = false
for (const { msgId, peer } of nodeMsgIds) {
const nodeMsg = (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgId])).msgList[0]
srcPeer ??= { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
}
nodeMsgArray.push(nodeMsg)
}
let retMsgIds: string[] = []
if (needSendSelf) {
for (const msg of nodeMsgArray) {
if (msg.peerUid === selfPeer.peerUid) {
retMsgIds.push(msg.msgId)
continue
}
const clonedMsg = await this.cloneMsg(msg)
if (clonedMsg) retMsgIds.push(clonedMsg.msgId)
}
} else {
retMsgIds = nodeMsgArray.map(msg => msg.msgId)
}
if (retMsgIds.length === 0) {
throw Error('转发消息失败,节点为空')
}
const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
returnMsg.msgShortId = this.ctx.store.createMsgShortId(destPeer, returnMsg.msgId)
return returnMsg
}
}
export class SendPrivateForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg
protected _handle(payload: Payload) {
payload.message_type = 'private'
return super._handle(payload)
}
}
export class SendGroupForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg
protected _handle(payload: Payload) {
payload.message_type = 'group'
return super._handle(payload)
}
}

View File

@@ -1,4 +1,4 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { unlink } from 'fs/promises'
import { checkFileReceived, uri2local } from '@/common/utils/file'
@@ -7,20 +7,24 @@ interface Payload {
group_id: number | string
content: string
image?: string
pinned?: number | string //扩展
confirm_required?: number | string //扩展
pinned: number | string //扩展
confirm_required: number | string //扩展
}
export class SendGroupNotice extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupNotice
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
content: Schema.string().required(),
image: Schema.string(),
pinned: Schema.union([Number, String]).default(0),
confirm_required: Schema.union([Number, String]).default(1)
})
async _handle(payload: Payload) {
if (!payload.content) {
throw new Error('参数 content 不能为空')
}
const groupCode = payload.group_id.toString()
const pinned = Number(payload.pinned ?? 0)
const confirmRequired = Number(payload.confirm_required ?? 1)
const pinned = +payload.pinned
const confirmRequired = +payload.confirm_required
let picInfo: { id: string, width: number, height: number } | undefined
if (payload.image) {

View File

@@ -1,6 +1,5 @@
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
@@ -13,13 +12,13 @@ export class SetEssenceMsg extends BaseAction<Payload, unknown> {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
return await this.ctx.ntGroupApi.addGroupEssence(
msg.Peer.peerUid,
msg.MsgId
msg.peer.peerUid,
msg.msgId
)
}
}

View File

@@ -1,16 +1,50 @@
import { GroupEssenceMsgRet } from '@/ntqqapi/api'
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types'
interface PayloadType {
group_id: number
pages?: number
interface Payload {
group_id: number | string
}
export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet | void> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg
interface EssenceMsg {
sender_id: number
sender_nick: string
sender_time: number
operator_id: number
operator_nick: string
operator_time: number
message_id: number
}
protected async _handle() {
throw '此 api 暂不支持'
export class GetGroupEssence extends BaseAction<Payload, EssenceMsg[]> {
actionName = ActionName.GoCQHTTP_GetEssenceMsgList
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString()
const peer = {
guildId: '',
chatType: ChatType.group,
peerUid: groupCode
}
const essence = await this.ctx.ntGroupApi.queryCachedEssenceMsg(groupCode)
const data: EssenceMsg[] = []
for (const item of essence.items) {
const { msgList } = await this.ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, String(item.msgSeq), '0')
const sourceMsg = msgList.find(e => e.msgRandom === String(item.msgRandom))
if (!sourceMsg) continue
data.push({
sender_id: +item.msgSenderUin,
sender_nick: item.msgSenderNick,
sender_time: +sourceMsg.msgTime,
operator_id: +item.opUin,
operator_nick: item.opNick,
operator_time: item.opTime,
message_id: this.ctx.store.createMsgShortId(peer, sourceMsg.msgId)
})
}
return data
}
}

View File

@@ -13,11 +13,16 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList
protected async _handle(payload: Payload) {
const groupMembers = await this.ctx.ntGroupApi.getGroupMembers(payload.group_id.toString())
const groupCode = payload.group_id.toString()
let groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
if (groupMembers.size === 0) {
await this.ctx.sleep(100)
groupMembers = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
}
const groupMembersArr = Array.from(groupMembers.values())
let _groupMembers = groupMembersArr.map(item => {
return OB11Entities.groupMember(payload.group_id.toString(), item)
const _groupMembers = groupMembersArr.map(item => {
return OB11Entities.groupMember(groupCode, item)
})
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>()
@@ -25,8 +30,8 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
for (let i = 0, len = _groupMembers.length; i < len; i++) {
// 保证基础数据有这个 同时避免群管插件过于依赖这个杀了
_groupMembers[i].join_time = date
_groupMembers[i].last_sent_time = date
_groupMembers[i].join_time ||= date
_groupMembers[i].last_sent_time ||= date
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i])
}
@@ -34,24 +39,24 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
const isPrivilege = selfRole === 3 || selfRole === 4
if (isPrivilege) {
const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(payload.group_id.toString())
const webGroupMembers = await this.ctx.ntWebApi.getGroupMembers(groupCode)
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
if (MemberData.join_time === date) {
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
return Array.from(MemberMap.values())
}
}

View File

@@ -6,7 +6,6 @@ class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg
protected _handle(payload: OB11PostSendMsg) {
delete (payload as Partial<OB11PostSendMsg>).user_id
payload.message_type = 'group'
return super._handle(payload)
}

View File

@@ -53,7 +53,7 @@ import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
import { HandleQuickOperation } from './go-cqhttp/QuickOperation'
import { SetEssenceMsg } from './go-cqhttp/SetEssenceMsg'
import { DelEssenceMsg } from './go-cqhttp/DelEssenceMsg'
import GetEvent from './llonebot/GetEvent'
import { GetEvent } from './llonebot/GetEvent'
import { DelGroupFile } from './go-cqhttp/DelGroupFile'
import { GetGroupSystemMsg } from './go-cqhttp/GetGroupSystemMsg'
import { CreateGroupFileFolder } from './go-cqhttp/CreateGroupFileFolder'
@@ -63,6 +63,10 @@ import { GetGroupRootFiles } from './go-cqhttp/GetGroupRootFiles'
import { SetOnlineStatus } from './llonebot/SetOnlineStatus'
import { SendGroupNotice } from './go-cqhttp/SendGroupNotice'
import { GetProfileLike } from './llonebot/GetProfileLike'
import { FetchEmojiLike } from './llonebot/FetchEmojiLike'
import { FetchCustomFace } from './llonebot/FetchCustomFace'
import { GetFriendMsgHistory } from './llonebot/GetFriendMsgHistory'
import { GetGroupFilesByFolder } from './go-cqhttp/GetGroupFilesByFolder'
export function initActionMap(adapter: Adapter) {
const actionHandlers = [
@@ -76,6 +80,9 @@ export function initActionMap(adapter: Adapter) {
new GetEvent(adapter),
new SetOnlineStatus(adapter),
new GetProfileLike(adapter),
new GetFriendMsgHistory(adapter),
new FetchEmojiLike(adapter),
new FetchCustomFace(adapter),
// onebot11
new SendLike(adapter),
new GetMsg(adapter),
@@ -109,7 +116,7 @@ export function initActionMap(adapter: Adapter) {
new SetMsgEmojiLike(adapter),
new ForwardFriendSingleMsg(adapter),
new ForwardGroupSingleMsg(adapter),
//以下为go-cqhttp api
// go-cqhttp
new GetGroupEssence(adapter),
new GetGroupHonorInfo(adapter),
new SendForwardMsg(adapter),
@@ -132,7 +139,8 @@ export function initActionMap(adapter: Adapter) {
new DelGroupFolder(adapter),
new GetGroupAtAllRemain(adapter),
new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter)
new SendGroupNotice(adapter),
new GetGroupFilesByFolder(adapter),
]
const actionMap = new Map<string, BaseAction<any, unknown>>()
for (const action of actionHandlers) {

View File

@@ -0,0 +1,18 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
count: number | string
}
export class FetchCustomFace extends BaseAction<Payload, string[]> {
actionName = ActionName.FetchCustomFace
payloadSchema = Schema.object({
count: Schema.union([Number, String]).default(48)
})
async _handle(payload: Payload) {
const ret = await this.ctx.ntMsgApi.fetchFavEmojiList(+payload.count)
return ret.emojiInfoList.map(e => e.url)
}
}

View File

@@ -0,0 +1,27 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { Dict } from 'cosmokit'
interface Payload {
emojiId: string
emojiType: string
message_id: string | number
count: string | number
}
export class FetchEmojiLike extends BaseAction<Payload, Dict> {
actionName = ActionName.FetchEmojiLike
payloadSchema = Schema.object({
emojiId: Schema.string().required(),
emojiType: Schema.string().required(),
message_id: Schema.union([Number, String]).required(),
count: Schema.union([Number, String]).default(20)
})
async _handle(payload: Payload) {
const msgInfo = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msgInfo) throw new Error('消息不存在')
const { msgSeq } = (await this.ctx.ntMsgApi.getMsgsByMsgId(msgInfo.peer, [msgInfo.msgId])).msgList[0]
return await this.ctx.ntMsgApi.getMsgEmojiLikesList(msgInfo.peer, msgSeq, payload.emojiId, payload.emojiType, +payload.count)
}
}

View File

@@ -11,7 +11,7 @@ interface Payload {
timeout: number
}
export default class GetEvent extends BaseAction<Payload, PostEventType[]> {
export class GetEvent extends BaseAction<Payload, PostEventType[]> {
actionName = ActionName.GetEvent
protected async _handle(payload: Payload): Promise<PostEventType[]> {

View File

@@ -0,0 +1,52 @@
import { BaseAction, Schema } from '../BaseAction'
import { OB11Message } from '@/onebot11/types'
import { ActionName } from '../types'
import { ChatType, RawMessage } from '@/ntqqapi/types'
import { OB11Entities } from '@/onebot11/entities'
import { filterNullable } from '@/common/utils/misc'
interface Payload {
user_id: number | string
message_seq?: number | string
message_id?: number | string
count: number | string
reverseOrder: boolean
}
interface Response {
messages: OB11Message[]
}
export class GetFriendMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GetFriendMsgHistory
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required(),
message_seq: Schema.union([Number, String]),
message_id: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false)
})
async _handle(payload: Payload): Promise<Response> {
const startMsgId = payload.message_seq ?? payload.message_id
let msgList: RawMessage[]
if (startMsgId) {
const msgInfo = await this.ctx.store.getMsgInfoByShortId(+startMsgId)
if (!msgInfo) throw new Error(`消息${startMsgId}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(msgInfo.peer, msgInfo.msgId, +payload.count)).msgList
} else {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error(`记录${payload.user_id}不存在`)
const isBuddy = await this.ctx.ntFriendApi.isBuddy(uid)
const peer = { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid: uid }
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList
}
if (msgList.length === 0) throw new Error('未找到消息')
if (payload.reverseOrder) msgList.reverse()
msgList.map(msg => {
msg.msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
})
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Entities.message(this.ctx, msg)))
return { messages: filterNullable(ob11MsgList) }
}
}

View File

@@ -1,6 +1,5 @@
import { ActionName } from '../types'
import { BaseAction } from '../BaseAction'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
@@ -13,11 +12,11 @@ class DeleteMsg extends BaseAction<Payload, void> {
if (!payload.message_id) {
throw new Error('参数message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) {
throw new Error(`消息${payload.message_id}不存在`)
}
const data = await this.ctx.ntMsgApi.recallMsg(msg.Peer, [msg.MsgId])
const data = await this.ctx.ntMsgApi.recallMsg(msg.peer, [msg.msgId])
if (data.result !== 0) {
this.ctx.logger.error('delete_msg', payload.message_id, data)
throw new Error(`消息撤回失败`)

View File

@@ -2,7 +2,6 @@ import { BaseAction } from '../BaseAction'
import { ChatType } from '@/ntqqapi/types'
import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
@@ -26,12 +25,12 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`)
}
const peer = await this.getTargetPeer(payload)
const ret = await this.ctx.ntMsgApi.forwardMsg(msg.Peer, peer, [msg.MsgId])
const ret = await this.ctx.ntMsgApi.forwardMsg(msg.peer, peer, [msg.msgId])
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`)
}

View File

@@ -2,7 +2,6 @@ import { BaseAction } from '../BaseAction'
import { OB11Message } from '../../types'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/messageUnique'
export interface PayloadType {
message_id: number | string
@@ -17,22 +16,21 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
if (!payload.message_id) {
throw new Error('参数message_id不能为空')
}
const msgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString())
const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(msgShortId || +payload.message_id)
if (!msgIdWithPeer) {
const msgInfo = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msgInfo) {
throw new Error('消息不存在')
}
const peer = {
guildId: '',
peerUid: msgIdWithPeer.Peer.peerUid,
chatType: msgIdWithPeer.Peer.chatType
peerUid: msgInfo.peer.peerUid,
chatType: msgInfo.peer.chatType
}
const msg = this.adapter.getMsgCache(msgIdWithPeer.MsgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgIdWithPeer.MsgId])).msgList[0]
const msg = this.adapter.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0]
const retMsg = await OB11Entities.message(this.ctx, msg)
if (!retMsg) {
throw new Error('消息为空')
}
retMsg.message_id = MessageUnique.createMsg(peer, msg.msgId)!
retMsg.message_id = this.ctx.store.createMsgShortId(peer, msg.msgId)
retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id
return retMsg

View File

@@ -1,25 +1,14 @@
import {
ChatType,
ElementType,
RawMessage,
SendMessageElement,
} from '@/ntqqapi/types'
import {
OB11MessageCustomMusic,
OB11MessageData,
OB11MessageDataType,
OB11MessageJson,
OB11MessageMusic,
OB11MessageNode,
OB11PostSendMsg,
} from '../../types'
import fs from 'node:fs'
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '@/common/utils/sign'
import { Peer } from '@/ntqqapi/types/msg'
import { MessageUnique } from '@/common/utils/messageUnique'
import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
interface ReturnData {
@@ -42,12 +31,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
payload.auto_escape === true || payload.auto_escape === 'true',
)
if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[])
return { message_id: returnMsg.msgShortId! }
} catch (e) {
throw '发送转发消息失败 ' + e
}
throw new Error('请使用 /send_group_forward_msg 或 /send_private_forward_msg 进行合并转发')
}
else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) {
const music = messages[0] as OB11MessageMusic
@@ -114,140 +98,10 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number {
if (Array.isArray(message)) {
return message.filter((msg) => msg.type == msgType).length
return message.filter((msg) => msg.type === msgType).length
}
return 0
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
this.ctx.logger.info('克隆的目标消息', msg)
const sendElements: SendMessageElement[] = []
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement)
}
if (sendElements.length === 0) {
this.ctx.logger.warn('需要clone的消息无法解析将会忽略掉', msg)
}
this.ctx.logger.info('克隆消息', sendElements)
try {
const peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
const nodeMsg = await this.ctx.ntMsgApi.sendMsg(peer, sendElements)
await this.ctx.sleep(400)
return nodeMsg
} catch (e) {
this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg)
}
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid,
}
let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
const nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
const nodeMsg = await MessageUnique.getMsgIdAndPeerByShortId(+nodeId) || await MessageUnique.getPeerByMsgId(nodeId)
if (!nodeMsg) {
this.ctx.logger.warn('转发消息失败,未找到消息', nodeId)
continue
}
nodeMsgIds.push(nodeMsg.MsgId)
}
else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx,
convertMessage2List(messageNode.data.content),
destPeer
)
this.ctx.logger.info('开始生成转发节点', sendElements)
const sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0
for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = []
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++
}
else {
sendElementsSplit[splitIndex].push(ele)
}
this.ctx.logger.info(sendElementsSplit)
}
// log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [])
if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles)
continue
}
nodeMsgIds.push(nodeMsg.msgId)
await this.ctx.sleep(400)
}
deleteAfterSentFiles.map((f) => fs.unlink(f, () => {
}))
} catch (e) {
this.ctx.logger.error('生成转发消息节点失败', e)
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer | null = null
let needSendSelf = false
for (const msgId of nodeMsgIds) {
const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId)
if (nodeMsgPeer) {
const nodeMsg = (await this.ctx.ntMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]
srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
}
nodeMsgArray.push(nodeMsg)
}
}
nodeMsgIds = nodeMsgArray.map((msg) => msg.msgId)
if (needSendSelf) {
for (const msg of nodeMsgArray) {
if (msg.peerUid === selfPeer.peerUid) continue
await this.cloneMsg(msg)
}
}
// elements之间用换行符分隔
// let _sendForwardElements: SendMessageElement[] = []
// for(let i = 0; i < sendForwardElements.length; i++){
// _sendForwardElements.push(sendForwardElements[i])
// _sendForwardElements.push(SendMsgElementConstructor.text("\n\n"))
// }
// const nodeMsg = await NTQQApi.sendMsg(selfPeer, _sendForwardElements, true);
// nodeIds.push(nodeMsg.msgId)
// await sleep(500);
// 开发转发
if (nodeMsgIds.length === 0) {
throw Error('转发消息失败,节点为空')
}
const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds)
returnMsg.msgShortId = MessageUnique.createMsg(destPeer, returnMsg.msgId)
return returnMsg
}
}
export default SendMsg

View File

@@ -1,6 +1,5 @@
import { ActionName } from '../types'
import { BaseAction } from '../BaseAction'
import { MessageUnique } from '@/common/utils/messageUnique'
interface Payload {
message_id: number | string
@@ -14,19 +13,19 @@ export class SetMsgEmojiLike extends BaseAction<Payload, unknown> {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
if (!payload.emoji_id) {
throw new Error('emojiId not found')
}
const msgData = (await this.ctx.ntMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList
const msgData = (await this.ctx.ntMsgApi.getMsgsByMsgId(msg.peer, [msg.msgId])).msgList
if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) {
throw new Error('find msg by msgid error')
}
return await this.ctx.ntMsgApi.setEmojiLike(
msg.Peer,
msg.peer,
msgData[0].msgSeq,
payload.emoji_id.toString(),
true

View File

@@ -21,6 +21,9 @@ export enum ActionName {
GetEvent = 'get_event',
SetOnlineStatus = 'set_online_status',
GetProfileLike = 'get_profile_like',
FetchEmojiLike = 'fetch_emoji_like',
FetchCustomFace = 'fetch_custom_face',
GetFriendMsgHistory = 'get_friend_msg_history',
// onebot 11
SendLike = 'send_like',
GetLoginInfo = 'get_login_info',
@@ -66,7 +69,7 @@ export enum ActionName {
GoCQHTTP_DownloadFile = 'download_file',
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
GoCQHTTP_GetForwardMsg = 'get_forward_msg',
GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list',
GoCQHTTP_GetEssenceMsgList = 'get_essence_msg_list',
GoCQHTTP_HandleQuickOperation = '.handle_quick_operation',
GetGroupHonorInfo = 'get_group_honor_info',
GoCQHTTP_SetEssenceMsg = 'set_essence_msg',
@@ -78,4 +81,5 @@ export enum ActionName {
GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
GoCQHTTP_SendGroupNotice = '_send_group_notice',
GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder'
}

View File

@@ -10,15 +10,12 @@ export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> {
try {
const qq = payload.user_id.toString()
const uid: string = await this.ctx.ntUserApi.getUidByUin(qq) || ''
const result = await this.ctx.ntUserApi.like(uid, +payload.times || 1)
if (result?.result !== 0) {
throw Error(result?.errMsg)
}
} catch (e) {
throw `点赞失败 ${e}`
const uin = payload.user_id.toString()
const uid = await this.ctx.ntUserApi.getUidByUin(uin)
if (!uid) throw new Error('无法获取用户信息')
const result = await this.ctx.ntUserApi.like(uid, +payload.times || 1)
if (result.result !== 0) {
throw new Error(result.errMsg)
}
return null
}

View File

@@ -12,7 +12,16 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> {
const approve = payload.approve?.toString() !== 'false'
await this.ctx.ntFriendApi.handleFriendRequest(payload.flag, approve)
const data = payload.flag.split('|')
if (data.length < 2) {
throw new Error('无效的flag')
}
const uid = data[0]
const reqTime = data[1]
await this.ctx.ntFriendApi.handleFriendRequest(uid, reqTime, approve)
if (payload.remark) {
await this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark)
}
return null
}
}

View File

@@ -13,7 +13,6 @@ import {
} from '../ntqqapi/types'
import { OB11GroupRequestEvent } from './event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from './event/request/OB11FriendRequest'
import { MessageUnique } from '../common/utils/messageUnique'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent'
import { selfInfo } from '../common/globalVars'
import { OB11Config, Config as LLOBConfig } from '../common/types'
@@ -37,7 +36,7 @@ declare module 'cordis' {
}
class OneBot11Adapter extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi']
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store']
public messages: Map<string, RawMessage> = new Map()
public startTime = 0
@@ -190,7 +189,7 @@ class OneBot11Adapter extends Service {
chatType: message.chatType,
peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
message.msgShortId = this.ctx.store.createMsgShortId(peer, message.msgId)
this.addMsgCache(message)
OB11Entities.message(this.ctx, message)
@@ -227,7 +226,11 @@ class OneBot11Adapter extends Service {
}
private handleRecallMsg(message: RawMessage) {
const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId)
const peer = {
peerUid: message.peerUid,
chatType: message.chatType
}
const oriMessageId = this.ctx.store.getShortIdByMsgInfo(peer, message.msgId)
if (!oriMessageId) {
return
}

View File

@@ -15,7 +15,6 @@ import {
FaceIndex,
GrayTipElementSubType,
Group,
Peer,
GroupMember,
RawMessage,
Sex,
@@ -26,7 +25,6 @@ import {
} from '../ntqqapi/types'
import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode'
import { MessageUnique } from '../common/utils/messageUnique'
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'
@@ -155,13 +153,21 @@ export namespace OB11Entities {
guildId: ''
}
try {
const { replayMsgSeq, replyMsgTime, senderUidStr } = replyElement
const { replayMsgSeq, replyMsgTime } = replyElement
const records = msg.records.find(msgRecord => msgRecord.msgId === replyElement.sourceMsgIdInRecords)
if (!records || !replyMsgTime || !senderUidStr) {
const senderUid = replyElement.senderUidStr || records?.senderUid
if (!records || !replyMsgTime || !senderUid) {
throw new Error('找不到回复消息')
}
const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, replayMsgSeq, replyMsgTime, [senderUidStr])
const replyMsg = msgList.find(msg => msg.msgRandom === records.msgRandom)
const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, replayMsgSeq, replyMsgTime, [senderUid])
let replyMsg: RawMessage | undefined
if (records.msgRandom !== '0') {
replyMsg = msgList.find(msg => msg.msgRandom === records.msgRandom)
} else {
ctx.logger.info('msgRandom is missing', replyElement, records)
replyMsg = msgList[0]
}
// 284840486: 合并消息内侧 消息具体定位不到
if (!replyMsg && msg.peerUin !== '284840486') {
@@ -171,7 +177,7 @@ export namespace OB11Entities {
messageSegment = {
type: OB11MessageDataType.reply,
data: {
id: MessageUnique.createMsg(peer, replyMsg ? replyMsg.msgId : records.msgId).toString()
id: ctx.store.createMsgShortId(peer, replyMsg ? replyMsg.msgId : records.msgId).toString()
}
}
} catch (e) {
@@ -191,7 +197,7 @@ export namespace OB11Entities {
file_size: fileSize,
}
}
MessageUnique.addFileCache({
ctx.store.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
@@ -219,7 +225,7 @@ export namespace OB11Entities {
file_size: fileSize,
}
}
MessageUnique.addFileCache({
ctx.store.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
@@ -244,7 +250,7 @@ export namespace OB11Entities {
file_size: fileSize,
}
}
MessageUnique.addFileCache({
ctx.store.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
@@ -268,7 +274,7 @@ export namespace OB11Entities {
file_size: fileSize,
}
}
MessageUnique.addFileCache({
ctx.store.addFileCache({
peerUid: msg.peerUid,
msgId: msg.msgId,
msgTime: +msg.msgTime,
@@ -388,7 +394,7 @@ export namespace OB11Entities {
)
}
}
if (grayTipElement.xmlElement?.templId === '10229') {
if (grayTipElement.xmlElement?.templId === '10229' || grayTipElement.jsonGrayTipElement?.busiId === '19324') {
const uin = +msg.peerUin || +(await ctx.ntUserApi.getUinByUid(msg.peerUid))
return new OB11FriendAddNoticeEvent(uin)
}
@@ -523,7 +529,7 @@ export namespace OB11Entities {
if (!replyMsgList?.length) {
return
}
const shortId = MessageUnique.getShortIdByMsgId(replyMsgList[0].msgId)
const shortId = ctx.store.getShortIdByMsgInfo(peer, replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid),
parseInt(senderUin),
@@ -574,32 +580,28 @@ export namespace OB11Entities {
)
}
}
if (grayTipElement.jsonGrayTipElement?.busiId === '2401') {
if (grayTipElement.jsonGrayTipElement?.busiId === '2401' && json.items[2]) {
ctx.logger.info('收到群精华消息', json)
const searchParams = new URL(json.items[0].jp).searchParams
const msgSeq = searchParams.get('msgSeq')!
const Group = searchParams.get('groupCode')
const Peer: Peer = {
const searchParams = new URL(json.items[2].jp).searchParams
const msgSeq = searchParams.get('seq')
const groupCode = searchParams.get('gc')
const msgRandom = searchParams.get('random')
if (!groupCode || !msgSeq || !msgRandom) return
const peer = {
guildId: '',
chatType: ChatType.group,
peerUid: Group!
peerUid: groupCode
}
const msgList = (await ctx.ntMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true))?.msgList
if (!msgList?.length) {
return
}
//const origMsg = await dbUtil.getMsgByLongId(msgList[0].msgId)
//const postMsg = await dbUtil.getMsgBySeqId(origMsg?.msgSeq!) ?? origMsg
// 如果 senderUin 为 0可能是 历史消息 或 自身消息
//if (msgList[0].senderUin === '0') {
//msgList[0].senderUin = postMsg?.senderUin ?? getSelfUin()
//}
const essence = await ctx.ntGroupApi.queryCachedEssenceMsg(groupCode, msgSeq, msgRandom)
const { msgList } = await ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, msgSeq, '0')
const sourceMsg = msgList.find(e => e.msgRandom === msgRandom)
if (!sourceMsg) return
return new OB11GroupEssenceEvent(
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgList[0].msgId)!,
parseInt(msgList[0].senderUin!)
ctx.store.getShortIdByMsgInfo(peer, sourceMsg.msgId)!,
parseInt(essence.items[0]?.msgSenderUin ?? sourceMsg.senderUin),
parseInt(essence.items[0]?.opUin ?? '0'),
)
// 获取MsgSeq+Peer可获取具体消息
}
if (grayTipElement.jsonGrayTipElement?.busiId === '2407') {
const memberUin = json.items[1].param[0]
@@ -702,10 +704,10 @@ export namespace OB11Entities {
sex: sex(member.sex!),
age: 0,
area: '',
level: '0',
level: String(member.memberLevel ?? 0),
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0,
join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取
join_time: member.joinTime,
last_sent_time: member.lastSpeakTime,
title_expire_time: 0,
unfriendly: false,
card_changeable: true,

View File

@@ -1,16 +1,20 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'
export class OB11GroupEssenceEvent extends OB11GroupNoticeEvent {
notice_type = 'essence'
message_id: number
sender_id: number
sub_type: 'add' | 'delete' = 'add'
group_id: number
user_id: number = 0
user_id: number
operator_id: number
constructor(groupId: number, message_id: number, sender_id: number) {
constructor(groupId: number, messageId: number, senderId: number, operatorId: number) {
super()
this.group_id = groupId
this.message_id = message_id
this.sender_id = sender_id
this.user_id = senderId
this.message_id = messageId
this.sender_id = senderId
this.operator_id = operatorId
}
}

View File

@@ -16,7 +16,6 @@ import {
import { decodeCQCode } from '../cqcode'
import { Peer } from '@/ntqqapi/types/msg'
import { SendElementEntities } from '@/ntqqapi/entities'
import { MessageUnique } from '@/common/utils/messageUnique'
import { selfInfo } from '@/common/globalVars'
import { uri2local } from '@/common/utils'
import { Context } from 'cordis'
@@ -87,14 +86,14 @@ export async function createSendElements(
break
case OB11MessageDataType.reply: {
if (sendMsg.data?.id) {
const replyMsgId = await MessageUnique.getMsgIdAndPeerByShortId(+sendMsg.data.id)
const replyMsgId = await ctx.store.getMsgInfoByShortId(+sendMsg.data.id)
if (!replyMsgId) {
ctx.logger.warn('回复消息不存在', replyMsgId)
continue
}
const replyMsg = (await ctx.ntMsgApi.getMsgsByMsgId(
replyMsgId.Peer,
[replyMsgId.MsgId!]
replyMsgId.peer,
[replyMsgId.msgId!]
)).msgList[0]
if (replyMsg) {
sendElements.push(
@@ -132,7 +131,8 @@ export async function createSendElements(
ctx,
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '',
sendMsg.data.subType || 0
sendMsg.data.subType || 0,
sendMsg.data.type === 'flash'
)
deleteAfterSentFiles.push(res.picElement.sourcePath)
sendElements.push(res)
@@ -180,6 +180,10 @@ export async function createSendElements(
sendElements.push(SendElementEntities.ark(await data))
}
break
case OB11MessageDataType.shake: {
sendElements.push(SendElementEntities.shake())
}
break
}
}
@@ -272,7 +276,7 @@ export async function sendMsg(
//log('设置消息超时时间', timeout)
const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, timeout)
if (returnMsg) {
returnMsg.msgShortId = MessageUnique.createMsg(peer, returnMsg.msgId)
returnMsg.msgShortId = ctx.store.createMsgShortId(peer, returnMsg.msgId)
ctx.logger.info('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg

View File

@@ -3,7 +3,6 @@ import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../helper/createMessage'
import { MessageUnique } from '@/common/utils/messageUnique'
import { isNullable } from 'cosmokit'
import { Context } from 'cordis'
@@ -88,18 +87,18 @@ async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOpera
}
if (msg.message_type === 'group') {
const groupMsgQuickAction = quickAction as QuickOperationGroupMessage
const rawMessage = await MessageUnique.getMsgIdAndPeerByShortId(+(msg.message_id ?? 0))
const rawMessage = await ctx.store.getMsgInfoByShortId(+(msg.message_id ?? 0))
if (!rawMessage) return
// handle group msg
if (groupMsgQuickAction.delete) {
ctx.ntMsgApi.recallMsg(peer, [rawMessage.MsgId]).catch(e => ctx.logger.error(e))
ctx.ntMsgApi.recallMsg(peer, [rawMessage.msgId]).catch(e => ctx.logger.error(e))
}
if (groupMsgQuickAction.kick) {
const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.msgId])
ctx.ntGroupApi.kickMember(peer.peerUid, [msgList[0].senderUid]).catch(e => ctx.logger.error(e))
}
if (groupMsgQuickAction.ban) {
const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.MsgId])
const { msgList } = await ctx.ntMsgApi.getMsgsByMsgId(peer, [rawMessage.msgId])
ctx.ntGroupApi.banMember(peer.peerUid, [
{
uid: msgList[0].senderUid,
@@ -112,8 +111,16 @@ async function handleMsg(ctx: Context, msg: OB11Message, quickAction: QuickOpera
async function handleFriendRequest(ctx: Context, request: OB11FriendRequestEvent, quickAction: QuickOperationFriendRequest) {
if (!isNullable(quickAction.approve)) {
// todo: set remark
ctx.ntFriendApi.handleFriendRequest(request.flag, quickAction.approve).catch(e => ctx.logger.error(e))
const data = request.flag.split('|')
if (data.length < 2) {
return
}
const uid = data[0]
const reqTime = data[1]
await ctx.ntFriendApi.handleFriendRequest(uid, reqTime, quickAction.approve).catch(e => ctx.logger.error(e))
if (!isNullable(quickAction.remark)) {
ctx.ntFriendApi.setBuddyRemark(uid, quickAction.remark).catch(e => ctx.logger.error(e))
}
}
}

View File

@@ -131,6 +131,7 @@ export enum OB11MessageDataType {
dice = 'dice',
RPS = 'rps',
contact = 'contact',
shake = 'shake',
}
export interface OB11MessageMFace {
@@ -187,6 +188,7 @@ export interface OB11MessageImage extends OB11MessageFileBase {
data: OB11MessageFileBase['data'] & {
summary?: string // 图片摘要
subType?: PicSubType
type?: 'flash' | 'show'
}
}
@@ -285,6 +287,11 @@ export interface OB11MessageContact {
}
}
export interface OB11MessageShake {
type: OB11MessageDataType.shake
data: {}
}
export type OB11MessageData =
| OB11MessageText
| OB11MessageFace
@@ -305,11 +312,12 @@ export type OB11MessageData =
| OB11MessageMarkdown
| OB11MessageForward
| OB11MessageContact
| OB11MessageShake
export interface OB11PostSendMsg {
message_type?: 'private' | 'group'
user_id: string
group_id?: string
user_id?: string | number
group_id?: string | number
message: OB11MessageMixType
messages?: OB11MessageMixType // 兼容 go-cqhttp
auto_escape?: boolean | string

View File

@@ -1 +1 @@
export const version = '3.32.7'
export const version = '3.33.2'

View File

@@ -1,18 +0,0 @@
import http from 'https'
function checkUrl(imageUrl) {
http
.get(imageUrl, (response) => {
console.log(response.statusCode)
})
.on('error', (e) => {
console.log(e)
})
}
checkUrl(
'https://gchat.qpic.cn/download?appid=1407&fileid=CgoxMzMyNTI0MjIxEhRrdaUgQP5MjweWa4uR8pviUDaGQhjcxQUg_wooiYTj39fphQNQgL2jAQ&spec=0&rkey=CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64',
)
checkUrl(
'https://multimedia.nt.qq.com.cn/download?appid=1407&fileid=CgoxMzMyNTI0MjIxEhRrdaUgQP5MjweWa4uR8pviUDaGQhjcxQUg_wooiYTj39fphQNQgL2jAQ&spec=0&rkey=CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64',
)

View File

@@ -15,7 +15,7 @@
"./src/common/*"
],
"@/onebot11/*": [
"./src/onebot11"
"./src/onebot11/*"
],
"@/ntqqapi/*": [
"./src/ntqqapi/*"
@@ -27,4 +27,4 @@
"src",
"scripts"
]
}
}