Compare commits

...

33 Commits

Author SHA1 Message Date
idranme
0876e4645f Merge pull request #456 from LLOneBot/dev
release: 3.34.0
2024-10-01 21:32:24 +08:00
idranme
a2f9128623 chore: v3.34.0 2024-10-01 21:25:19 +08:00
idranme
e313b2b3e6 feat 2024-10-01 21:16:39 +08:00
idranme
a7d86f8fe0 refactor 2024-10-01 21:09:27 +08:00
idranme
496d56f297 feat 2024-09-30 00:49:58 +08:00
idranme
ed2f554d4e refactor 2024-09-28 22:00:05 +08:00
idranme
36d990e328 Merge pull request #452 from LLOneBot/dev
release: 3.33.10
2024-09-28 14:40:11 +08:00
idranme
0ceef4d4c0 chore: v3.33.10 2024-09-28 14:37:44 +08:00
idranme
35bf4f001b feat: _get_group_notice API 2024-09-28 14:35:06 +08:00
idranme
544682fe41 fix 2024-09-28 12:54:02 +08:00
idranme
3da49fbfba optimize 2024-09-27 18:37:47 +08:00
idranme
d5875c9e5b Merge pull request #451 from LLOneBot/dev
release: 3.33.9
2024-09-27 16:53:44 +08:00
idranme
7895644156 chore: v3.33.9 2024-09-27 16:51:49 +08:00
idranme
f092626ede fix 2024-09-27 16:22:50 +08:00
idranme
a58fb31f8e Merge pull request #448 from LLOneBot/dev
release: 3.33.8
2024-09-26 12:57:16 +08:00
idranme
fe85e277f1 chore: v3.33.8 2024-09-26 12:54:30 +08:00
idranme
5217638b46 feat 2024-09-26 01:52:47 +08:00
idranme
f68b707e1c optimize 2024-09-25 22:34:59 +08:00
idranme
c24ce6ec65 adjustment of get_friends_with_category API returns 2024-09-25 22:04:52 +08:00
idranme
f9270c38cf Merge pull request #444 from LLOneBot/dev
release: 3.33.7
2024-09-25 14:59:34 +08:00
idranme
fd478cdaed chore: v3.33.7 2024-09-25 14:55:05 +08:00
idranme
517b233496 fix 2024-09-25 14:52:04 +08:00
idranme
1045c94a91 feat: get_group_file_url API 2024-09-25 12:13:28 +08:00
idranme
032ac85c04 refactor 2024-09-24 19:59:07 +08:00
idranme
1e35ffd7e6 optimize 2024-09-24 14:18:44 +08:00
idranme
e5ab6134cd Merge pull request #441 from LLOneBot/dev
release: 3.33.6
2024-09-23 23:43:50 +08:00
idranme
a95ae44614 chore: v3.33.6 2024-09-23 23:36:10 +08:00
idranme
3dc9940ac9 feat 2024-09-23 23:34:52 +08:00
idranme
277e418cf3 refactor 2024-09-23 22:10:12 +08:00
idranme
24f09d485e Merge pull request #438 from LLOneBot/dev
release: 3.33.5
2024-09-22 21:31:55 +08:00
idranme
3394823719 chore: v3.33.5 2024-09-22 21:26:37 +08:00
idranme
afa06f0760 fix 2024-09-22 21:18:59 +08:00
idranme
4f9e465fb2 optimize 2024-09-22 20:37:20 +08:00
66 changed files with 1331 additions and 1121 deletions

View File

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

View File

@@ -18,7 +18,6 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.6.0", "@minatojs/driver-sqlite": "^4.6.0",
"compressing": "^1.10.1",
"cordis": "^3.18.1", "cordis": "^3.18.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.2", "cosmokit": "^1.6.2",
@@ -33,7 +32,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.26", "@types/fluent-ffmpeg": "^2.1.26",
"@types/node": "^20.14.15", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
@@ -41,7 +40,7 @@
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"protobufjs-cli": "^1.1.3", "protobufjs-cli": "^1.1.3",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"vite": "^5.4.7", "vite": "^5.4.8",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.5.0" "packageManager": "yarn@4.5.0"

View File

@@ -190,26 +190,3 @@ export async function uri2local(uri: string, filename?: string, needExt?: boolea
return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false } return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false }
} }
export async function copyFolder(sourcePath: string, destPath: string) {
try {
const entries = await fsPromise.readdir(sourcePath, { withFileTypes: true })
await fsPromise.mkdir(destPath, { recursive: true })
for (const entry of entries) {
const srcPath = path.join(sourcePath, entry.name)
const dstPath = path.join(destPath, entry.name)
if (entry.isDirectory()) {
await copyFolder(srcPath, dstPath)
} else {
try {
await fsPromise.copyFile(srcPath, dstPath)
} catch (error) {
console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`)
// 这里可以决定是否要继续复制其他文件
}
}
}
} catch (error) {
console.error('复制文件夹时出错:', error)
}
}

View File

@@ -2,24 +2,7 @@ import fs from 'fs'
import path from 'node:path' import path from 'node:path'
import { getConfigUtil } from '../config' import { getConfigUtil } from '../config'
import { LOG_DIR } from '../globalVars' import { LOG_DIR } from '../globalVars'
import { Dict } from 'cosmokit' import { inspect } from 'node:util'
function truncateString(obj: Dict | null, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength)
}
})
}
return obj
}
export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-') export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
@@ -29,9 +12,8 @@ export function log(...msg: unknown[]) {
} }
let logMsg = '' let logMsg = ''
for (const msgItem of msg) { for (const msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === 'object') { if (typeof msgItem === 'object') {
logMsg += JSON.stringify(truncateString(msgItem)) + ' ' logMsg += inspect(msgItem, { depth: 10, compact: true, breakLength: Infinity }) + ' '
continue continue
} }
logMsg += msgItem + ' ' logMsg += msgItem + ' '

View File

@@ -38,3 +38,10 @@ export function mergeNewProperties(newObj: Dict, oldObj: Dict) {
export function filterNullable<T>(array: T[]) { export function filterNullable<T>(array: T[]) {
return array.filter(e => !isNullable(e)) as NonNullable<T>[] return array.filter(e => !isNullable(e)) as NonNullable<T>[]
} }
export function parseBool(value: string) {
if (['', 'true', '1'].includes(value)) {
return true
}
return false
}

View File

@@ -1,9 +1,8 @@
import path from 'node:path' import path from 'node:path'
import compressing from 'compressing'
import { writeFile } from 'node:fs/promises' import { writeFile } from 'node:fs/promises'
import { version } from '../../version' import { version } from '../../version'
import { copyFolder, log, fetchFile } from '.' import { log, fetchFile } from '.'
import { PLUGIN_DIR, TEMP_DIR } from '../globalVars' import { TEMP_DIR } from '../globalVars'
const downloadMirrorHosts = ['https://ghp.ci/'] const downloadMirrorHosts = ['https://ghp.ci/']
const releasesMirrorHosts = ['https://kkgithub.com'] const releasesMirrorHosts = ['https://kkgithub.com']
@@ -27,49 +26,21 @@ export async function checkNewVersion() {
return { result: false, version: version } return { result: false, version: version }
} }
export async function upgradeLLOneBot() { export async function upgradeLLOneBot(): Promise<boolean> {
const latestVersion = await getRemoteVersion() const latestVersion = await getRemoteVersion()
if (latestVersion && latestVersion != '') { if (latestVersion && latestVersion != '') {
const downloadUrl = `https://github.com/LLOneBot/LLOneBot/releases/download/v${latestVersion}/LLOneBot.zip` const downloadUrl = `https://github.com/LLOneBot/LLOneBot/releases/download/v${latestVersion}/LLOneBot.zip`
const filePath = path.join(TEMP_DIR, './update-' + latestVersion + '.zip') const filePath = path.join(TEMP_DIR, './update-' + latestVersion + '.zip')
let downloadSuccess = false
// 多镜像下载 // 多镜像下载
for (const mirrorGithub of downloadMirrorHosts) { for (const mirrorGithub of downloadMirrorHosts) {
try { try {
const res = await fetchFile(mirrorGithub + downloadUrl) const res = await fetchFile(mirrorGithub + downloadUrl)
await writeFile(filePath, res.data) await writeFile(filePath, res.data)
downloadSuccess = true return globalThis.LiteLoader.api.plugin.install(filePath)
break
} catch (e) { } catch (e) {
log('llonebot upgrade error', e) log('llonebot upgrade error', e)
} }
} }
if (!downloadSuccess) {
log('llonebot upgrade error', 'download failed')
return false
}
const temp_ver_dir = path.join(TEMP_DIR, 'LLOneBot' + latestVersion)
const uncompressedPromise = async function () {
return new Promise<boolean>(resolve => {
compressing.zip
.uncompress(filePath, temp_ver_dir)
.then(() => {
resolve(true)
})
.catch(reason => {
log('llonebot upgrade failed, ', reason)
if (reason?.errno == -4082) {
resolve(true)
}
resolve(false)
})
})
}
const uncompressedResult = await uncompressedPromise()
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR)
return uncompressedResult
} }
return false return false
} }

View File

@@ -191,7 +191,9 @@ function onLoad() {
ctx.plugin(SQLiteDriver, { ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`) path: path.join(dbDir, `${selfInfo.uin}.db`)
}) })
ctx.plugin(Store) ctx.plugin(Store, {
msgCacheExpire: config.msgCacheExpire! * 1000
})
ctx.start() ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => { ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config) ctx.parallel('llonebot/config-updated', config)

View File

@@ -1,4 +1,4 @@
import { Peer } from '@/ntqqapi/types' import { Peer, RawMessage } from '@/ntqqapi/types'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { LimitedHashTable } from '@/common/utils/table' import { LimitedHashTable } from '@/common/utils/table'
import { FileCacheV2 } from '@/common/types' import { FileCacheV2 } from '@/common/types'
@@ -24,13 +24,15 @@ interface MsgInfo {
peer: Peer peer: Peer
} }
export default class Store extends Service { class Store extends Service {
static inject = ['database', 'model'] static inject = ['database', 'model']
private cache: LimitedHashTable<string, number> private cache: LimitedHashTable<string, number>
private messages: Map<string, RawMessage>
constructor(protected ctx: Context) { constructor(protected ctx: Context, public config: Store.Config) {
super(ctx, 'store', true) super(ctx, 'store', true)
this.cache = new LimitedHashTable<string, number>(1000) this.cache = new LimitedHashTable(1000)
this.messages = new Map()
this.initDatabase() this.initDatabase()
} }
@@ -123,4 +125,29 @@ export default class Store extends Service {
getFileCacheById(fileUuid: string) { getFileCacheById(fileUuid: string) {
return this.ctx.database.get('file_v2', { fileUuid }) return this.ctx.database.get('file_v2', { fileUuid })
} }
async addMsgCache(msg: RawMessage) {
const expire = this.config.msgCacheExpire
if (expire === 0) {
return
}
const id = msg.msgId
this.messages.set(id, msg)
setTimeout(() => {
this.messages.delete(id)
}, expire)
}
getMsgCache(msgId: string) {
return this.messages.get(msgId)
}
} }
namespace Store {
export interface Config {
/** 单位为毫秒 */
msgCacheExpire: number
}
}
export default Store

View File

@@ -16,14 +16,12 @@ import path from 'node:path'
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { RkeyManager } from '@/ntqqapi/helper/rkey' import { RkeyManager } from '@/ntqqapi/helper/rkey'
import { getSession } from '@/ntqqapi/wrapper'
import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg' import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type' import { fileTypeFromFile } from 'file-type'
import { copyFile, stat, unlink } from 'node:fs/promises' import { copyFile, stat, unlink } from 'node:fs/promises'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { TEMP_DIR } from '@/common/globalVars'
declare module 'cordis' { declare module 'cordis' {
interface Context { interface Context {
@@ -40,40 +38,29 @@ export class NTQQFileApi extends Service {
this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey') this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey')
} }
async getVideoUrl(peer: Peer, msgId: string, elementId: string) { async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string | undefined> {
const session = getSession() const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
if (session) { peer,
return (await session.getRichMediaService().getVideoPlayUrlV2( msgId,
peer, elemId: elementId,
msgId, videoCodecFormat: 0,
elementId, params: {
0, downSourceType: 1,
{ downSourceType: 1, triggerType: 1 } triggerType: 1
)).urlResult.domainUrl[0]?.url
} else {
const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
peer,
msgId,
elemId: elementId,
videoCodecFormat: 0,
exParams: {
downSourceType: 1,
triggerType: 1
},
}, null])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
} }
return data.urlResult.domainUrl[0]?.url }])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
} }
return data.urlResult.domainUrl[0]?.url
} }
async getFileType(filePath: string) { async getFileType(filePath: string) {
return fileTypeFromFile(filePath) return fileTypeFromFile(filePath)
} }
// 上传文件到QQ的文件夹 /** 上传文件到 QQ 的文件夹 */
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) { async uploadFile(filePath: string, elementType = ElementType.Pic, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath) const fileMd5 = await calculateFileMD5(filePath)
let fileName = path.basename(filePath) let fileName = path.basename(filePath)
if (!fileName.includes('.')) { if (!fileName.includes('.')) {
@@ -107,8 +94,8 @@ export class NTQQFileApi extends Service {
chatType: ChatType, chatType: ChatType,
peerUid: string, peerUid: string,
elementId: string, elementId: string,
thumbPath: string, thumbPath = '',
sourcePath: string, sourcePath = '',
timeout = 1000 * 60 * 2, timeout = 1000 * 60 * 2,
force = false force = false
) { ) {
@@ -147,13 +134,7 @@ export class NTQQFileApi extends Service {
timeout timeout
} }
) )
let filePath = data.notifyInfo.filePath return data.notifyInfo.filePath
if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath)
// 下载路径是下载文件夹的相对路径
}
return filePath
} }
async getImageSize(filePath: string) { async getImageSize(filePath: string) {
@@ -181,8 +162,8 @@ export class NTQQFileApi extends Service {
if (url) { if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = parsedUrl.searchParams.get('appid') const imageAppid = parsedUrl.searchParams.get('appid')
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid) const isNTPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) { if (isNTPic) {
let rkey = parsedUrl.searchParams.get('rkey') let rkey = parsedUrl.searchParams.get('rkey')
if (rkey) { if (rkey) {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
@@ -201,6 +182,27 @@ export class NTQQFileApi extends Service {
this.ctx.logger.error('图片url获取失败', element) this.ctx.logger.error('图片url获取失败', element)
return '' return ''
} }
async downloadFileForModelId(peer: Peer, fileModelId: string, timeout = 2 * Time.minute) {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
'nodeIKernelRichMediaService/downloadFileForModelId',
[
{
peer,
fileModelIdList: [fileModelId],
save_path: ''
},
null,
],
{
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.fileModelId === fileModelId,
timeout,
afterFirstCmd: false
}
)
return data.notifyInfo.filePath
}
} }
export class NTQQFileCacheApi extends Service { export class NTQQFileCacheApi extends Service {
@@ -222,7 +224,7 @@ export class NTQQFileCacheApi extends Service {
} }
scanCache() { scanCache() {
invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { classNameIsRegister: true }) invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { registerEvent: true })
return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [null, null], { timeout: 300 * Time.second }) return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [null, null], { timeout: 300 * Time.second })
} }

View File

@@ -1,9 +1,7 @@
import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types' import { Friend, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { invoke, NTMethod, NTClass } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType } from '../services'
import { Dict, pick } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
declare module 'cordis' { declare module 'cordis' {
@@ -61,7 +59,7 @@ export class NTQQFriendApi extends Service {
} }
} }
async getBuddyV2(refresh = false): Promise<FriendV2[]> { async getBuddyV2(refresh = false): Promise<SimpleInfo[]> {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
@@ -102,61 +100,24 @@ export class NTQQFriendApi extends Service {
return retMap return retMap
} }
async getBuddyV2ExWithCate(refresh = false) { async getBuddyV2WithCate(refresh = false) {
const session = getSession() const data = await invoke<{
if (session) { buddyCategory: CategoryFriend[]
const uids: string[] = [] userSimpleInfos: Record<string, SimpleInfo>
const categoryMap: Map<string, Dict> = new Map() }>(
const buddyService = session.getBuddyService() 'getBuddyList',
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data [refresh],
uids.push( {
...buddyListV2.flatMap(item => { className: NTClass.NODE_STORE_API,
item.buddyUids.forEach(uid => { cbCmd: ReceiveCmdS.FRIENDS,
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName }) afterFirstCmd: false,
})
return item.buddyUids
}))
const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map()
for (const item of data.buddyCategory) {
category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName']))
} }
return Object.values(data.userSimpleInfos) )
.filter(v => v.baseInfo && category.get(v.baseInfo.categoryId)?.buddyUids.includes(v.uid!)) return data
.map(value => {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: category.get(value.baseInfo.categoryId)?.categroyName
}
})
}
} }
async isBuddy(uid: string): Promise<boolean> { async isBuddy(uid: string): Promise<boolean> {
const session = getSession() return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }])
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
}
} }
async getBuddyRecommendContact(uin: string) { async getBuddyRecommendContact(uin: string) {

View File

@@ -6,8 +6,10 @@ import {
GroupNotifies, GroupNotifies,
GroupRequestOperateTypes, GroupRequestOperateTypes,
GetFileListParam, GetFileListParam,
OnGroupFileInfoUpdateParams, PublishGroupBulletinReq,
PublishGroupBulletinReq GroupAllInfo,
GroupFileInfo,
GroupBulletinListResult
} from '../types' } from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
@@ -65,32 +67,30 @@ export class NTQQGroupApi extends Service {
return result.result.infos return result.result.infos
} }
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { async getGroupMember(groupCode: string, memberUinOrUid: string) {
const groupCodeStr = groupCode.toString() if (!this.groupMembers.has(groupCode)) {
const memberUinOrUidStr = memberUinOrUid.toString()
if (!this.groupMembers.has(groupCodeStr)) {
try { try {
// 更新群成员列表 // 更新群成员列表
this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr)) this.groupMembers.set(groupCode, await this.getGroupMembers(groupCode))
} }
catch (e) { catch (e) {
return null return
} }
} }
let members = this.groupMembers.get(groupCodeStr)! let members = this.groupMembers.get(groupCode)!
const getMember = () => { const getMember = () => {
let member: GroupMember | undefined = undefined let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUidStr)) { if (isNumeric(memberUinOrUid)) {
member = Array.from(members.values()).find(member => member.uin === memberUinOrUidStr) member = Array.from(members.values()).find(member => member.uin === memberUinOrUid)
} else { } else {
member = members.get(memberUinOrUidStr) member = members.get(memberUinOrUid)
} }
return member return member
} }
let member = getMember() let member = getMember()
if (!member) { if (!member) {
this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr)) this.groupMembers.set(groupCode, await this.getGroupMembers(groupCode))
members = this.groupMembers.get(groupCodeStr)! members = this.groupMembers.get(groupCode)!
member = getMember() member = getMember()
} }
return member return member
@@ -106,7 +106,7 @@ export class NTQQGroupApi extends Service {
} }
async getSingleScreenNotifies(num: number) { async getSingleScreenNotifies(num: number) {
invoke(ReceiveCmdS.GROUP_NOTIFY, [], { classNameIsRegister: true }) invoke(ReceiveCmdS.GROUP_NOTIFY, [], { registerEvent: true })
return (await invoke<GroupNotifies>( return (await invoke<GroupNotifies>(
'nodeIKernelGroupService/getSingleScreenNotifies', 'nodeIKernelGroupService/getSingleScreenNotifies',
[{ doubt: false, startSeq: '', number: num }, null], [{ doubt: false, startSeq: '', number: num }, null],
@@ -158,12 +158,7 @@ export class NTQQGroupApi extends Service {
} }
} }
async kickMember( async kickMember(groupCode: string, kickUids: string[], refuseForever = false, kickReason = '') {
groupCode: string,
kickUids: string[],
refuseForever = false,
kickReason = '',
) {
const session = getSession() const session = getSession()
if (session) { if (session) {
return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason) return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
@@ -279,8 +274,8 @@ export class NTQQGroupApi extends Service {
} }
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) { async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { classNameIsRegister: true }) invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { registerEvent: true })
const data = await invoke<{ fileInfo: OnGroupFileInfoUpdateParams }>( const data = await invoke<{ fileInfo: GroupFileInfo }>(
'nodeIKernelRichMediaService/getGroupFileList', 'nodeIKernelRichMediaService/getGroupFileList',
[ [
{ {
@@ -295,7 +290,7 @@ export class NTQQGroupApi extends Service {
cmdCB: (payload, result) => payload.fileInfo.reqId === result cmdCB: (payload, result) => payload.fileInfo.reqId === result
} }
) )
return data.fileInfo.item return data.fileInfo
} }
async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) { async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
@@ -333,4 +328,53 @@ export class NTQQGroupApi extends Service {
} }
}, null]) }, null])
} }
async getGroupAllInfo(groupCode: string, timeout = 1000) {
invoke('nodeIKernelGroupListener/onGroupAllInfoChange', [], { registerEvent: true })
return await invoke<{ groupAll: GroupAllInfo }>(
'nodeIKernelGroupService/getGroupAllInfo',
[
{
groupCode,
source: 4
},
null
],
{
cbCmd: 'nodeIKernelGroupListener/onGroupAllInfoChange',
afterFirstCmd: false,
cmdCB: payload => payload.groupAll.groupCode === groupCode,
timeout
}
)
}
async getGroupBulletinList(groupCode: string) {
invoke('nodeIKernelGroupListener/onGetGroupBulletinListResult', [], { registerEvent: true })
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke<{
groupCode: string
context: string
result: GroupBulletinListResult
}>(
'nodeIKernelGroupService/getGroupBulletinList',
[{
groupCode,
psKey,
context: '',
req: {
startIndex: -1,
num: 20,
needInstructionsForJoinGroup: 1,
needPublisherInfo: 1
}
}],
{
cbCmd: 'nodeIKernelGroupListener/onGetGroupBulletinListResult',
cmdCB: payload => payload.groupCode === groupCode,
afterFirstCmd: false
}
)
}
} }

View File

@@ -27,17 +27,12 @@ export class NTQQMsgApi extends Service {
} }
} }
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean = true) { async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
const session = getSession()
const emojiType = emojiId.length > 3 ? '2' : '1' const emojiType = emojiId.length > 3 ? '2' : '1'
if (session) { return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }])
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiType, setEmoji)
} else {
return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }, null])
}
} }
async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
@@ -58,59 +53,41 @@ export class NTQQMsgApi extends Service {
} }
async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) { async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null]) return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }])
} }
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) { async getMsgsByMsgId(peer: Peer, msgIds: string[]) {
if (!peer) throw new Error('peer is not allowed') if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed') if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession() return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }])
if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }, null])
}
} }
async getMsgHistory(peer: Peer, msgId: string, cnt: number, isReverseOrder: boolean = false) { async getMsgHistory(peer: Peer, msgId: string, cnt: number, isReverseOrder: boolean = false) {
const session = getSession()
// 消息时间从旧到新 // 消息时间从旧到新
if (session) { return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }])
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, cnt, isReverseOrder)
} else {
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }, null])
}
} }
async recallMsg(peer: Peer, msgIds: string[]) { async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession() return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }])
if (session) {
return session.getMsgService().recallMsg(peer, msgIds)
} else {
return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }, null])
}
} }
async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) { async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
const msgId = await this.generateMsgUniqueId(peer.chatType) const uniqueId = await this.generateMsgUniqueId(peer.chatType)
peer.guildId = msgId peer.guildId = uniqueId
const data = await invoke<{ msgList: RawMessage[] }>( const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/sendMsg', 'nodeIKernelMsgService/sendMsg',
[ [{
{ msgId: '0',
msgId: '0', peer,
peer, msgElements,
msgElements, msgAttributeInfos: new Map()
msgAttributeInfos: new Map() }],
},
null
],
{ {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => { cmdCB: payload => {
for (const msgRecord of payload.msgList) { for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) { if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
return true return true
} }
} }
@@ -119,22 +96,35 @@ export class NTQQMsgApi extends Service {
timeout timeout
} }
) )
return data.msgList.find(msgRecord => msgRecord.guildId === msgId) return data.msgList.find(msgRecord => msgRecord.guildId === uniqueId)
} }
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession() const uniqueId = await this.generateMsgUniqueId(destPeer.chatType)
if (session) { destPeer.guildId = uniqueId
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], []) const data = await invoke<{ msgList: RawMessage[] }>(
} else { 'nodeIKernelMsgService/forwardMsg',
return await invoke(NTMethod.FORWARD_MSG, [{ [{
msgIds, msgIds,
srcContact: srcPeer, srcContact: srcPeer,
dstContacts: [destPeer], dstContacts: [destPeer],
commentElements: [], commentElements: [],
msgAttributeInfos: new Map(), msgAttributeInfos: new Map(),
}, null]) }],
} {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
return true
}
}
return false
}
}
)
return data.msgList.filter(msgRecord => msgRecord.guildId === uniqueId)
} }
async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> { async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
@@ -145,27 +135,24 @@ export class NTQQMsgApi extends Service {
const selfUid = selfInfo.uid const selfUid = selfInfo.uid
const data = await invoke<{ msgList: RawMessage[] }>( const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/multiForwardMsgWithComment', 'nodeIKernelMsgService/multiForwardMsgWithComment',
[ [{
{ msgInfos,
msgInfos, srcContact: srcPeer,
srcContact: srcPeer, dstContact: destPeer,
dstContact: destPeer, commentElements: [],
commentElements: [], msgAttributeInfos: new Map(),
msgAttributeInfos: new Map(), }],
},
null,
],
{ {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => { cmdCB: payload => {
for (const msgRecord of payload.msgList) { for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) { if (msgRecord.peerUid === destPeer.peerUid && msgRecord.senderUid === selfUid) {
return true return true
} }
} }
return false return false
}, }
} }
) )
for (const msg of data.msgList) { for (const msg of data.msgList) {
@@ -174,10 +161,10 @@ export class NTQQMsgApi extends Service {
continue continue
} }
const forwardData = JSON.parse(arkElement.arkElement!.bytesData) const forwardData = JSON.parse(arkElement.arkElement!.bytesData)
if (forwardData.app != 'com.tencent.multimsg') { if (forwardData.app !== 'com.tencent.multimsg') {
continue continue
} }
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) { if (msg.peerUid === destPeer.peerUid && msg.senderUid === selfUid) {
return msg return msg
} }
} }
@@ -240,7 +227,7 @@ export class NTQQMsgApi extends Service {
isIncludeCurrent: true, isIncludeCurrent: true,
pageLimit: 1, pageLimit: 1,
} }
}, null]) }])
} }
async setMsgRead(peer: Peer) { async setMsgRead(peer: Peer) {
@@ -254,7 +241,7 @@ export class NTQQMsgApi extends Service {
emojiId, emojiId,
emojiType, emojiType,
cnt: count cnt: count
}, null]) }])
} }
async fetchFavEmojiList(count: number) { async fetchFavEmojiList(count: number) {
@@ -267,6 +254,41 @@ export class NTQQMsgApi extends Service {
} }
async generateMsgUniqueId(chatType: number) { async generateMsgUniqueId(chatType: number) {
return await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType }]) const time = await this.getServerTime()
const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType, time }])
if (typeof uniqueId === 'string') {
return uniqueId
} else {
const random = Math.trunc(Math.random() * 100)
return `${Date.now()}${random}`
}
}
async queryMsgsById(chatType: ChatType, msgId: string) {
const msgTime = this.getMsgTimeFromId(msgId)
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId,
msgTime: '0',
msgSeq: '0',
params: {
chatInfo: {
peerUid: '',
chatType
},
filterMsgToTime: msgTime,
filterMsgFromTime: msgTime,
isIncludeCurrent: true,
pageLimit: 1,
}
}])
}
getMsgTimeFromId(msgId: string) {
// 小概率相差1毫秒
return String(BigInt(msgId) >> 32n)
}
async getServerTime() {
return await invoke('nodeIKernelMSFService/getServerTime', [null])
} }
} }

View File

@@ -1,10 +1,9 @@
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfo, UserDetailSource, ProfileBizType, SimpleInfo } from '../types'
import { invoke } from '../ntcall' import { invoke } from '../ntcall'
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { UserDetailSource, ProfileBizType } from '../services' import { isNullable, Time } from 'cosmokit'
import { Time } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
@@ -35,17 +34,14 @@ export class NTQQUserApi extends Service {
} }
async fetchUserDetailInfo(uid: string) { async fetchUserDetailInfo(uid: string) {
const result = await invoke<{ info: UserDetailInfoListenerArg }>( const result = await invoke<{ info: UserDetailInfo }>(
'nodeIKernelProfileService/fetchUserDetailInfo', 'nodeIKernelProfileService/fetchUserDetailInfo',
[ [{
{ callFrom: 'BuddyProfileStore',
callFrom: 'BuddyProfileStore', uid: [uid],
uid: [uid], source: UserDetailSource.KSERVER,
source: UserDetailSource.KSERVER, bizList: [ProfileBizType.KALL]
bizList: [ProfileBizType.KALL] }],
},
null
],
{ {
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged', cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
@@ -71,13 +67,10 @@ export class NTQQUserApi extends Service {
} }
const result = await invoke<{ info: User }>( const result = await invoke<{ info: User }>(
'nodeIKernelProfileService/getUserDetailInfoWithBizInfo', 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
[ [{
{ uid,
uid, bizList: [0]
bizList: [0] }],
},
null,
],
{ {
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged', cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
@@ -130,9 +123,7 @@ export class NTQQUserApi extends Service {
} }
async getUidByUinV1(uin: string) { async getUidByUinV1(uin: string) {
const session = getSession() let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) { if (!uid) {
for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转 for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
for (const member of membersList.values()) { for (const member of membersList.values()) {
@@ -157,30 +148,24 @@ export class NTQQUserApi extends Service {
return uid return uid
} }
async getUidByUinV2(uin: string) { async getUidByUinV2(uin: string, groupCode?: string) {
const session = getSession() let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin)
if (session) { if (uid) return uid
let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin) uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid if (uid) return uid
uid = (await session.getProfileService().getUidByUin('FriendsServiceImpl', [uin])).get(uin) uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid if (uid) return uid
uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin) const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid
if (uid) return uid if (!unveifyUid.includes('*')) return unveifyUid
} else { if (groupCode) {
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin) const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uin)
if (uid) return uid return member?.uid
uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid
uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid
} }
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
} }
async getUidByUin(uin: string) { async getUidByUin(uin: string, groupCode?: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.getUidByUinV2(uin) return this.getUidByUinV2(uin, groupCode)
} }
return this.getUidByUinV1(uin) return this.getUidByUinV1(uin)
} }
@@ -188,20 +173,14 @@ export class NTQQUserApi extends Service {
async getUserDetailInfoByUinV2(uin: string) { async getUserDetailInfoByUinV2(uin: string) {
return await invoke<UserDetailInfoByUinV2>( return await invoke<UserDetailInfoByUinV2>(
'nodeIKernelProfileService/getUserDetailInfoByUin', 'nodeIKernelProfileService/getUserDetailInfoByUin',
[ [{ uin }]
{ uin },
null,
],
) )
} }
async getUserDetailInfoByUin(uin: string) { async getUserDetailInfoByUin(uin: string) {
return await invoke<UserDetailInfoByUin>( return await invoke<UserDetailInfoByUin>(
'nodeIKernelProfileService/getUserDetailInfoByUin', 'nodeIKernelProfileService/getUserDetailInfoByUin',
[ [{ uin }]
{ uin },
null,
],
) )
} }
@@ -209,31 +188,21 @@ export class NTQQUserApi extends Service {
const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }]) const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])
let uin = ret.uinInfo.get(uid) let uin = ret.uinInfo.get(uid)
if (!uin) { if (!uin) {
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换 uin = (await this.getUserDetailInfo(uid)).uin
} }
return uin return uin
} }
async getUinByUidV2(uid: string) { async getUinByUidV2(uid: string) {
const session = getSession() let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
if (uin) return uin
uin = (await session.getProfileService().getUinByUid('FriendsServiceImpl', [uid])).get(uid)
if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
} else {
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换 uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin
return uin return uin
} }
@@ -249,17 +218,14 @@ export class NTQQUserApi extends Service {
if (session) { if (session) {
return await session.getTicketService().forceFetchClientKey('') return await session.getTicketService().forceFetchClientKey('')
} else { } else {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ domain: '' }, null]) return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }, null])
} }
} }
async getSelfNick(refresh = false) { async getSelfNick(refresh = true) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) { if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const userInfo = await this.getUserDetailInfo(selfInfo.uid) const { profiles } = await this.getUserSimpleInfo(selfInfo.uid)
if (userInfo) { selfInfo.nick = profiles[selfInfo.uid].coreInfo.nick
Object.assign(selfInfo, { nick: userInfo.nick })
return userInfo.nick
}
} }
return selfInfo.nick return selfInfo.nick
} }
@@ -288,4 +254,44 @@ export class NTQQUserApi extends Service {
} }
}, null]) }, null])
} }
async getUserSimpleInfo(uid: string, force = true) {
return await invoke<{ profiles: Record<string, SimpleInfo> }>(
'nodeIKernelProfileService/getUserSimpleInfo',
[{
uids: [uid],
force
}],
{
cbCmd: 'onProfileSimpleChanged',
afterFirstCmd: false,
cmdCB: payload => !isNullable(payload.profiles[uid]),
}
)
}
async getCoreAndBaseInfo(uids: string[]) {
return await invoke(
'nodeIKernelProfileService/getCoreAndBaseInfo',
[{
uids,
callFrom: 'nodeStore'
}]
)
}
async getRobotUinRange() {
const data = await invoke(
'nodeIKernelRobotService/getRobotUinRange',
[{
req: {
justFetchMsgConfig: '1',
type: 1,
version: 0,
aioKeywordVersion: 0
}
}]
)
return data.response.robotUinRanges
}
} }

View File

@@ -2,7 +2,6 @@ import { unlink } from 'node:fs/promises'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook' import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { Config as LLOBConfig } from '../common/types' import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc' import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall' import { NTMethod } from './ntcall'
import { import {
@@ -13,7 +12,8 @@ import {
GroupMember, GroupMember,
CategoryFriend, CategoryFriend,
SimpleInfo, SimpleInfo,
ChatType ChatType,
BuddyReqType
} from './types' } from './types'
import { selfInfo } from '../common/globalVars' import { selfInfo } from '../common/globalVars'
import { version } from '../version' import { version } from '../version'
@@ -24,25 +24,26 @@ declare module 'cordis' {
app: Core app: Core
} }
interface Events { interface Events {
'nt/message-created': (input: RawMessage[]) => void 'nt/message-created': (input: RawMessage) => void
'nt/message-deleted': (input: RawMessage) => void 'nt/message-deleted': (input: RawMessage) => void
'nt/message-sent': (input: RawMessage) => void 'nt/message-sent': (input: RawMessage) => void
'nt/group-notify': (input: GroupNotify[]) => void 'nt/group-notify': (input: GroupNotify) => void
'nt/friend-request': (input: FriendRequest[]) => void 'nt/friend-request': (input: FriendRequest) => void
'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void 'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
'nt/system-message-created': (input: Uint8Array) => void 'nt/system-message-created': (input: Uint8Array) => void
} }
} }
class Core extends Service { class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi'] static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi', 'store']
public startTime = 0
constructor(protected ctx: Context, public config: Core.Config) { constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true) super(ctx, 'app', true)
} }
public start() { public start() {
llonebotError.otherError = '' this.startTime = Date.now()
this.registerListener() this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`) this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => { this.ctx.on('llonebot/config-updated', input => {
@@ -63,7 +64,7 @@ class Core extends Service {
uids = payload.data.flatMap(item => item.buddyList.map(e => e.uid)) uids = payload.data.flatMap(item => item.buddyList.map(e => e.uid))
} }
for (const uid of uids) { for (const uid of uids) {
this.ctx.ntMsgApi.activateChat({ peerUid: uid, chatType: ChatType.friend }) this.ctx.ntMsgApi.activateChat({ peerUid: uid, chatType: ChatType.C2C })
} }
this.ctx.logger.info('好友列表变动', uids.length) this.ctx.logger.info('好友列表变动', uids.length)
}) })
@@ -116,12 +117,12 @@ class Core extends Service {
if (activatedPeerUids.includes(contact.id)) continue if (activatedPeerUids.includes(contact.id)) continue
activatedPeerUids.push(contact.id) activatedPeerUids.push(contact.id)
const peer = { peerUid: contact.id, chatType: contact.chatType } const peer = { peerUid: contact.id, chatType: contact.chatType }
if (contact.chatType === ChatType.temp) { if (contact.chatType === ChatType.TempC2CFromGroup) {
this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => { this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => {
this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => { this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
const lastTempMsg = msgList.at(-1) const lastTempMsg = msgList.at(-1)
if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) { if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) {
this.ctx.parallel('nt/message-created', [lastTempMsg!]) this.ctx.parallel('nt/message-created', lastTempMsg!)
} }
}) })
}) })
@@ -136,12 +137,12 @@ class Core extends Service {
registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => { registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string const peerUid = payload[0] as string
this.ctx.logger.info('激活的聊天窗口被删除,准备重新激活', peerUid) this.ctx.logger.info('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend let chatType = ChatType.C2C
if (isNumeric(peerUid)) { if (isNumeric(peerUid)) {
chatType = ChatType.group chatType = ChatType.Group
} }
else if (!(await this.ctx.ntFriendApi.isBuddy(peerUid))) { else if (!(await this.ctx.ntFriendApi.isBuddy(peerUid))) {
chatType = ChatType.temp chatType = ChatType.TempC2CFromGroup
} }
const peer = { peerUid, chatType } const peer = { peerUid, chatType }
await this.ctx.sleep(1000) await this.ctx.sleep(1000)
@@ -161,7 +162,16 @@ class Core extends Service {
}) })
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => { registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
this.ctx.parallel('nt/message-created', payload.msgList) for (const message of payload.msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < this.startTime / 1000) {
continue
}
if (message.senderUin && message.senderUin !== '0') {
this.ctx.store.addMsgCache(message)
}
this.ctx.parallel('nt/message-created', message)
}
}) })
const sentMsgIds = new Map<string, boolean>() const sentMsgIds = new Map<string, boolean>()
@@ -199,23 +209,31 @@ class Core extends Service {
} catch (e) { } catch (e) {
return return
} }
const list = notifies.filter(v => { for (const notify of notifies) {
const flag = v.group.groupCode + '|' + v.seq + '|' + v.type const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (groupNotifyFlags.includes(flag)) { const notifyTime = parseInt(notify.seq) / 1000
return false if (groupNotifyFlags.includes(flag) || notifyTime < this.startTime) {
continue
} }
groupNotifyFlags.push(flag) groupNotifyFlags.push(flag)
return true this.ctx.parallel('nt/group-notify', notify)
}) }
this.ctx.parallel('nt/group-notify', list)
} }
}) })
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs) for (const req of payload.data.buddyReqs) {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.MeInitiatorWaitPeerConfirm)) {
continue
}
if (+req.reqTime < this.startTime / 1000) {
continue
}
this.ctx.parallel('nt/friend-request', req)
}
}) })
invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { classNameIsRegister: true }) invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { registerEvent: true })
registerReceiveHook<{ registerReceiveHook<{
msgBuf: number[] msgBuf: number[]

View File

@@ -1,5 +1,6 @@
import ffmpeg from 'fluent-ffmpeg' import ffmpeg from 'fluent-ffmpeg'
import faceConfig from './helper/face_config.json' import faceConfig from './helper/face_config.json'
import pathLib from 'node:path'
import { import {
AtType, AtType,
ElementType, ElementType,
@@ -22,16 +23,14 @@ import { encodeSilk } from '../common/utils/audio'
import { Context } from 'cordis' import { Context } from 'cordis'
import { isNullable } from 'cosmokit' import { isNullable } from 'cosmokit'
//export const mFaceCache = new Map<string, string>() // emojiId -> faceName export namespace SendElement {
export namespace SendElementEntities {
export function text(content: string): SendTextElement { export function text(content: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.Text,
elementId: '', elementId: '',
textElement: { textElement: {
content, content,
atType: AtType.notAt, atType: AtType.Unknown,
atUid: '', atUid: '',
atTinyId: '', atTinyId: '',
atNtUid: '', atNtUid: '',
@@ -41,7 +40,7 @@ export namespace SendElementEntities {
export function at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement { export function at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.Text,
elementId: '', elementId: '',
textElement: { textElement: {
content: display, content: display,
@@ -55,7 +54,7 @@ export namespace SendElementEntities {
export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return { return {
elementType: ElementType.REPLY, elementType: ElementType.Reply,
elementId: '', elementId: '',
replyElement: { replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq replayMsgSeq: msgSeq, // raw.msgSeq
@@ -67,7 +66,7 @@ export namespace SendElementEntities {
} }
export async function pic(ctx: Context, picPath: string, summary = '', subType: 0 | 1 = 0, isFlashPic?: boolean): 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) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(picPath, ElementType.Pic, subType)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常,大小为 0' throw '文件异常,大小为 0'
} }
@@ -80,7 +79,7 @@ export namespace SendElementEntities {
fileName: fileName, fileName: fileName,
sourcePath: path, sourcePath: path,
original: true, original: true,
picType: imageSize.type === 'gif' ? PicType.gif : PicType.jpg, picType: imageSize.type === 'gif' ? PicType.GIF : PicType.JPEG,
picSubType: subType, picSubType: subType,
fileUuid: '', fileUuid: '',
fileSubId: '', fileSubId: '',
@@ -90,7 +89,7 @@ export namespace SendElementEntities {
} }
ctx.logger.info('图片信息', picElement) ctx.logger.info('图片信息', picElement)
return { return {
elementType: ElementType.PIC, elementType: ElementType.Pic,
elementId: '', elementId: '',
picElement, picElement,
} }
@@ -103,7 +102,7 @@ export namespace SendElementEntities {
throw new Error('文件异常,大小为 0') throw new Error('文件异常,大小为 0')
} }
const element: SendFileElement = { const element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.File,
elementId: '', elementId: '',
fileElement: { fileElement: {
fileName, fileName,
@@ -122,7 +121,7 @@ export namespace SendElementEntities {
throw `文件${filePath}异常,不存在` throw `文件${filePath}异常,不存在`
} }
ctx.logger.info('复制视频到QQ目录', filePath) ctx.logger.info('复制视频到QQ目录', filePath)
const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.VIDEO) const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video)
ctx.logger.info('复制视频到QQ目录完成', path) ctx.logger.info('复制视频到QQ目录完成', path)
if (fileSize === 0) { if (fileSize === 0) {
@@ -132,7 +131,6 @@ export namespace SendElementEntities {
if (fileSize > 1024 * 1024 * maxMB) { if (fileSize > 1024 * 1024 * maxMB) {
throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} }
const pathLib = require('path')
let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumbDir = pathLib.dirname(thumbDir) thumbDir = pathLib.dirname(thumbDir)
// log("thumb 目录", thumb) // log("thumb 目录", thumb)
@@ -200,7 +198,7 @@ export namespace SendElementEntities {
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
const element: SendVideoElement = { const element: SendVideoElement = {
elementType: ElementType.VIDEO, elementType: ElementType.Video,
elementId: '', elementId: '',
videoElement: { videoElement: {
fileName: fileName || _fileName, fileName: fileName || _fileName,
@@ -212,17 +210,7 @@ export namespace SendElementEntities {
thumbSize, thumbSize,
thumbWidth: videoInfo.width, thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height, thumbHeight: videoInfo.height,
fileSize: '' + fileSize, fileSize: String(fileSize),
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// sourceVideoCodecFormat: 2
}, },
} }
ctx.logger.info('videoElement', element) ctx.logger.info('videoElement', element)
@@ -235,7 +223,7 @@ export namespace SendElementEntities {
throw '语音转换失败, 请检查语音文件是否正常' throw '语音转换失败, 请检查语音文件是否正常'
} }
// log("生成语音", silkPath, duration); // log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.PTT) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.Ptt)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
@@ -243,14 +231,13 @@ export namespace SendElementEntities {
unlink(silkPath) unlink(silkPath)
} }
return { return {
elementType: ElementType.PTT, elementType: ElementType.Ptt,
elementId: '', elementId: '',
pttElement: { pttElement: {
fileName: fileName, fileName: fileName,
filePath: path, filePath: path,
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, fileSize: String(fileSize),
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration, duration: duration,
formatType: 1, formatType: 1,
voiceType: 1, voiceType: 1,
@@ -279,7 +266,7 @@ export namespace SendElementEntities {
faceType = 3; faceType = 3;
} }
return { return {
elementType: ElementType.FACE, elementType: ElementType.Face,
elementId: '', elementId: '',
faceElement: { faceElement: {
faceIndex: faceId, faceIndex: faceId,
@@ -295,7 +282,8 @@ export namespace SendElementEntities {
export function mface(emojiPackageId: number, emojiId: string, key: string, summary?: string): SendMarketFaceElement { export function mface(emojiPackageId: number, emojiId: string, key: string, summary?: string): SendMarketFaceElement {
return { return {
elementType: ElementType.MFACE, elementType: ElementType.MarketFace,
elementId: '',
marketFaceElement: { marketFaceElement: {
imageWidth: 300, imageWidth: 300,
imageHeight: 300, imageHeight: 300,
@@ -312,10 +300,10 @@ export namespace SendElementEntities {
// 随机1到6 // 随机1到6
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1 if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return { return {
elementType: ElementType.FACE, elementType: ElementType.Face,
elementId: '', elementId: '',
faceElement: { faceElement: {
faceIndex: FaceIndex.dice, faceIndex: FaceIndex.Dice,
faceType: 3, faceType: 3,
faceText: '[骰子]', faceText: '[骰子]',
packId: '1', packId: '1',
@@ -334,7 +322,7 @@ export namespace SendElementEntities {
// 实际测试并不能控制结果 // 实际测试并不能控制结果
if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1 if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return { return {
elementType: ElementType.FACE, elementType: ElementType.Face,
elementId: '', elementId: '',
faceElement: { faceElement: {
faceIndex: FaceIndex.RPS, faceIndex: FaceIndex.RPS,
@@ -353,7 +341,7 @@ export namespace SendElementEntities {
export function ark(data: string): SendArkElement { export function ark(data: string): SendArkElement {
return { return {
elementType: ElementType.ARK, elementType: ElementType.Ark,
elementId: '', elementId: '',
arkElement: { arkElement: {
bytesData: data, bytesData: data,
@@ -365,7 +353,7 @@ export namespace SendElementEntities {
export function shake(): SendFaceElement { export function shake(): SendFaceElement {
return { return {
elementType: ElementType.FACE, elementType: ElementType.Face,
elementId: '', elementId: '',
faceElement: { faceElement: {
faceIndex: 1, faceIndex: 1,

View File

@@ -31,30 +31,18 @@ export class RkeyManager {
isExpired(): boolean { isExpired(): boolean {
const now = new Date().getTime() / 1000 const now = new Date().getTime() / 1000
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`)
return now > this.rkeyData.expired_time return now > this.rkeyData.expired_time
} }
async refreshRkey() { async refreshRkey() {
//刷新rkey
this.rkeyData = await this.fetchServerRkey() this.rkeyData = await this.fetchServerRkey()
} }
async fetchServerRkey() { async fetchServerRkey(): Promise<ServerRkeyData> {
return new Promise<ServerRkeyData>((resolve, reject) => { const response = await fetch(this.serverUrl)
fetch(this.serverUrl) if (!response.ok) {
.then(response => { throw new Error(response.statusText)
if (!response.ok) { }
return reject(response.statusText) // 请求失败,返回错误信息 return response.json()
}
return response.json() // 解析 JSON 格式的响应体
})
.then(data => {
resolve(data)
})
.catch(error => {
reject(error)
})
})
} }
} }

View File

@@ -26,7 +26,6 @@ export enum ReceiveCmdS {
SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH = 'nodeIKernelStorageCleanListener/onFinishScan', CACHE_SCAN_FINISH = 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE = 'onSkeyUpdate',
} }
type NTReturnData = [ type NTReturnData = [
@@ -96,7 +95,7 @@ export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi') const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
if (!isLogger) { if (!isLogger) {
try { try {
logHook && log('call NTQQ api', thisArg, args) logHook && log('call NTQQ api', args)
} catch (e) { } } catch (e) { }
if (!onlyLog) { if (!onlyLog) {
try { try {

View File

@@ -13,7 +13,8 @@ import {
NodeIKernelUixConvertService, NodeIKernelUixConvertService,
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService NodeIKernelTipOffService,
NodeIKernelRobotService
} from './services' } from './services'
export enum NTClass { export enum NTClass {
@@ -39,7 +40,6 @@ export enum NTMethod {
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild', MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg', RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes', EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
SELF_INFO = 'fetchAuthData', SELF_INFO = 'fetchAuthData',
FILE_TYPE = 'getFileType', FILE_TYPE = 'getFileType',
@@ -93,12 +93,13 @@ interface NTService {
nodeIKernelRichMediaService: NodeIKernelRichMediaService nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService
} }
interface InvokeOptions<ReturnType> { interface InvokeOptions<ReturnType> {
className?: NTClass className?: NTClass
channel?: NTChannel channel?: NTChannel
classNameIsRegister?: boolean registerEvent?: boolean
cbCmd?: string | string[] cbCmd?: string | string[]
cmdCB?: (payload: ReturnType, result: unknown) => boolean cmdCB?: (payload: ReturnType, result: unknown) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
@@ -115,7 +116,7 @@ export function invoke<
const timeout = options.timeout ?? 5000 const timeout = options.timeout ?? 5000
const afterFirstCmd = options.afterFirstCmd ?? true const afterFirstCmd = options.afterFirstCmd ?? true
let eventName = className + '-' + channel[channel.length - 1] let eventName = className + '-' + channel[channel.length - 1]
if (options.classNameIsRegister) { if (options.registerEvent) {
eventName += '-register' eventName += '-register'
} }
return new Promise<R>((resolve, reject) => { return new Promise<R>((resolve, reject) => {
@@ -159,9 +160,9 @@ export function invoke<
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
log('ntqq api call failed,', method, res) log('ntqq api call failed,', method, args, res)
clearTimeout(timeoutId) clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`) reject(`ntqq api call failed, ${method}, ${res?.errMsg}`)
} }
} }
} }

View File

@@ -1,10 +1,6 @@
import { BuddyListReqType } from '@/ntqqapi/types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export interface NodeIKernelBuddyService { export interface NodeIKernelBuddyService {
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & { getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
data: { data: {

View File

@@ -1,42 +1,18 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types' import { ElementType, MessageElement, Peer, RawMessage, QueryMsgsParams, TmpChatInfoApi } from '@/ntqqapi/types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
export interface QueryMsgsParams {
chatInfo: Peer
filterMsgType: []
filterSendersUid: string[]
filterMsgFromTime: string
filterMsgToTime: string
pageLimit: number
isReverseOrder: boolean
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi {
errMsg: string
result: number
tmpChatInfo?: TmpChatInfo
}
export interface TmpChatInfo {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
export interface NodeIKernelMsgService { export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string generateMsgUniqueId(chatType: number, time: string): string
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<unknown, unknown>): Promise<GeneralCallResult> sendMsg(msgId: string, peer: Peer, msgElements: MessageElement[], map: Map<unknown, unknown>): Promise<GeneralCallResult>
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult> recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult> setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult> forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult & {
detailErr: Map<unknown, unknown>
}>
forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult> forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult>
@@ -97,7 +73,7 @@ export interface NodeIKernelMsgService {
downloadRichMedia(...args: unknown[]): unknown downloadRichMedia(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown setMsgEmojiLikes(...args: unknown[]): Promise<GeneralCallResult>
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{ getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number result: number

View File

@@ -1,19 +1,6 @@
import { SimpleInfo } from '../types' import { SimpleInfo } from '../types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}
export interface NodeIKernelProfileService { export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string, string>> getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string, string>>

View File

@@ -0,0 +1,13 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelRobotService {
getRobotUinRange(req: unknown): Promise<GeneralCallResult & {
response: {
version: number
robotUinRanges: {
minUin: string
maxUin: string
}[]
}
}>
}

View File

@@ -9,3 +9,4 @@ export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService' export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelRobotService'

View File

@@ -22,9 +22,9 @@ export interface Group {
hasModifyConfGroupName: boolean hasModifyConfGroupName: boolean
remarkName: string remarkName: string
hasMemo: boolean hasMemo: boolean
groupShutupExpireTime: string //"0", groupShutupExpireTime: string
personShutupExpireTime: string //"0", personShutupExpireTime: string
discussToGroupUin: string //"0", discussToGroupUin: string
discussToGroupMaxMsgSeq: number discussToGroupMaxMsgSeq: number
discussToGroupTime: number discussToGroupTime: number
groupFlagExt: number //1073938496, groupFlagExt: number //1073938496,
@@ -32,8 +32,8 @@ export interface Group {
groupCreditLevel: number //0, groupCreditLevel: number //0,
groupFlagExt3: number //0, groupFlagExt3: number //0,
groupOwnerId: { groupOwnerId: {
memberUin: string //"0", memberUin: string
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q" memberUid: string
} }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string createTime: string
@@ -78,3 +78,97 @@ export interface PublishGroupBulletinReq {
pinned: number pinned: number
confirmRequired: number confirmRequired: number
} }
export interface GroupAllInfo {
groupCode: string
ownerUid: string
groupFlag: number
groupFlagExt: number
maxMemberNum: number
memberNum: number
groupOption: number
classExt: number
groupName: string
fingerMemo: string
groupQuestion: string
certType: number
shutUpAllTimestamp: number
shutUpMeTimestamp: number //解除禁言时间
groupTypeFlag: number
privilegeFlag: number
groupSecLevel: number
groupFlagExt3: number
isConfGroup: number
isModifyConfGroupFace: number
isModifyConfGroupName: number
noFigerOpenFlag: number
noCodeFingerOpenFlag: number
groupFlagExt4: number
groupMemo: string
cmdUinMsgSeq: number
cmdUinJoinTime: number
cmdUinUinFlag: number
cmdUinMsgMask: number
groupSecLevelInfo: number
cmdUinPrivilege: number
cmdUinFlagEx2: number
appealDeadline: number
remarkName: number
isTop: boolean
richFingerMemo: string
groupAnswer: string
joinGroupAuth: string
isAllowModifyConfGroupName: number
}
export interface GroupBulletinListResult {
groupCode: string
srvCode: number
readOnly: number
role: number
inst: unknown[]
feeds: {
uin: string
feedId: string
publishTime: string
msg: {
text: string
textFace: string
pics: {
id: string
width: number
height: number
}[]
title: string
}
type: number
fn: number
cn: number
vn: number
settings: {
isShowEditCard: number
remindTs: number
tipWindowType: number
confirmRequired: number
}
pinned: number
readNum: number
is_read: number
is_all_confirm: number
}[]
groupInfo: {
groupCode: string
classId: number
}
gln: number
tst: number
publisherInfos: {
uin: string
nick: string
avatar: string
}[]
server_time: string
svrt: string
nextIndex: number
jointime: string
}

View File

@@ -1,125 +1,108 @@
import { GroupMemberRole } from './group' import { GroupMemberRole } from './group'
import { GeneralCallResult } from '../services'
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
folderId?: string
}
export enum ElementType { export enum ElementType {
UNKNOWN = 0, Text = 1,
TEXT = 1, Pic = 2,
PIC = 2, File = 3,
FILE = 3, Ptt = 4,
PTT = 4, Video = 5,
VIDEO = 5, Face = 6,
FACE = 6, Reply = 7,
REPLY = 7, GrayTip = 8,
WALLET = 9, Ark = 10,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字 MarketFace = 11,
ARK = 10, LiveGift = 12,
MFACE = 11, StructLongMsg = 13,
LIVEGIFT = 12, Markdown = 14,
STRUCTLONGMSG = 13, Giphy = 15,
MARKDOWN = 14, MultiForward = 16,
GIPHY = 15, InlineKeyboard = 17,
MULTIFORWARD = 16, Calendar = 19,
INLINEKEYBOARD = 17, YoloGameResult = 20,
INTEXTGIFT = 18, AvRecord = 21,
CALENDAR = 19, TofuRecord = 23,
YOLOGAMERESULT = 20, FaceBubble = 27,
AVRECORD = 21, ShareLocation = 28,
FEED = 22, TaskTopMsg = 29,
TOFURECORD = 23, RecommendedMsg = 43,
ACEBUBBLE = 24, ActionBar = 44
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
} }
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT elementType: ElementType.Text
elementId: '' elementId: ''
textElement: TextElement textElement: TextElement
} }
export interface SendPttElement { export interface SendPttElement {
elementType: ElementType.PTT elementType: ElementType.Ptt
elementId: '' elementId: ''
pttElement: { pttElement: Partial<PttElement>
fileName: string
filePath: string
md5HexStr: string
fileSize: number
duration: number // 单位是秒
formatType: number
voiceType: number
voiceChangeType: number
canConvert2Text: boolean
waveAmplitudes: number[]
fileSubId: ''
playState: number
autoConvertText: number
}
}
export enum PicType {
gif = 2000,
jpg = 1000,
}
export enum PicSubType {
normal = 0, // 普通图片,大图
face = 1, // 表情包小图
} }
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC elementType: ElementType.Pic
elementId: '' elementId: ''
picElement: { picElement: Partial<PicElement>
md5HexStr: string
fileSize: number | string
picWidth: number
picHeight: number
fileName: string
sourcePath: string
original: boolean
picType: PicType
picSubType: PicSubType
fileUuid: string
fileSubId: string
thumbFileSize: number
summary: string
}
} }
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY elementType: ElementType.Reply
elementId: '' elementId: ''
replyElement: Partial<ReplyElement> replyElement: Partial<ReplyElement>
} }
export interface SendFaceElement { export interface SendFaceElement {
elementType: ElementType.FACE elementType: ElementType.Face
elementId: '' elementId: ''
faceElement: FaceElement faceElement: FaceElement
} }
export interface SendMarketFaceElement { export interface SendMarketFaceElement {
elementType: ElementType.MFACE elementType: ElementType.MarketFace
elementId: ''
marketFaceElement: MarketFaceElement marketFaceElement: MarketFaceElement
} }
export interface SendFileElement {
elementType: ElementType.File
elementId: ''
fileElement: FileElement
}
export interface SendVideoElement {
elementType: ElementType.Video
elementId: ''
videoElement: VideoElement
}
export interface SendArkElement {
elementType: ElementType.Ark
elementId: ''
arkElement: ArkElement
}
export type SendMessageElement =
| SendTextElement
| SendPttElement
| SendPicElement
| SendReplyElement
| SendFaceElement
| SendMarketFaceElement
| SendFileElement
| SendVideoElement
| SendArkElement
export enum AtType {
Unknown,
All,
One,
}
export interface TextElement { export interface TextElement {
content: string content: string
atType: number atType: AtType
atUid: string atUid: string
atTinyId: string atTinyId: string
atNtUid: string atNtUid: string
@@ -156,47 +139,6 @@ export interface FileElement {
fileBizId?: number fileBizId?: number
} }
export interface SendFileElement {
elementType: ElementType.FILE
elementId: ''
fileElement: FileElement
}
export interface SendVideoElement {
elementType: ElementType.VIDEO
elementId: ''
videoElement: VideoElement
}
export interface SendArkElement {
elementType: ElementType.ARK
elementId: ''
arkElement: ArkElement
}
export type SendMessageElement =
| SendTextElement
| SendPttElement
| SendPicElement
| SendReplyElement
| SendFaceElement
| SendMarketFaceElement
| SendFileElement
| SendVideoElement
| SendArkElement
export enum AtType {
notAt = 0,
atAll = 1,
atUser = 2,
}
export enum ChatType {
friend = 1,
group = 2,
temp = 100,
}
export interface PttElement { export interface PttElement {
canConvert2Text: boolean canConvert2Text: boolean
duration: number // 秒数 duration: number // 秒数
@@ -207,7 +149,7 @@ export interface PttElement {
fileSize: string // "4261" fileSize: string // "4261"
fileSubId: string // "0" fileSubId: string // "0"
fileUuid: string // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV" fileUuid: string // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string // 1 formatType: number // 1
invalidState: number // 0 invalidState: number // 0
md5HexStr: string // "e4d09c784d5a2abcb2f9980bdc7acfe6" md5HexStr: string // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number // 0 playState: number // 0
@@ -218,6 +160,7 @@ export interface PttElement {
voiceChangeType: number // 0 voiceChangeType: number // 0
voiceType: number // 0 voiceType: number // 0
waveAmplitudes: number[] waveAmplitudes: number[]
autoConvertText: number
} }
export interface ArkElement { export interface ArkElement {
@@ -229,6 +172,16 @@ export interface ArkElement {
export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn' export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn'
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn' export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export enum PicType {
GIF = 2000,
JPEG = 1000,
}
export enum PicSubType {
Normal = 0, // 普通图片,大图
Face = 1, // 表情包小图
}
export interface PicElement { export interface PicElement {
picSubType: PicSubType picSubType: PicSubType
picType: PicType // 有这玩意儿吗 picType: PicType // 有这玩意儿吗
@@ -245,22 +198,22 @@ export interface PicElement {
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
REVOKE = 1, Revoke = 1,
PROCLAMATION = 2, Proclamation = 2,
EMOJIREPLY = 3, EmojiReply = 3,
GROUP = 4, Group = 4,
BUDDY = 5, Buddy = 5,
FEED = 6, Feed = 6,
ESSENCE = 7, Essence = 7,
GROUPNOTIFY = 8, GroupNotify = 8,
BUDDYNOTIFY = 9, BuddyNotify = 9,
FILE = 10, File = 10,
FEEDCHANNELMSG = 11, FeedChannelMsg = 11,
XMLMSG = 12, XmlMsg = 12,
LOCALMSG = 13, LocalMsg = 13,
BLOCK = 14, Block = 14,
AIOOP = 15, AioOp = 15,
WALLET = 16, Wallet = 16,
JSON = 17, JSON = 17,
} }
@@ -290,7 +243,7 @@ export interface GrayTipElement {
export enum FaceIndex { export enum FaceIndex {
dice = 358, Dice = 358,
RPS = 359, // 石头剪刀布 RPS = 359, // 石头剪刀布
} }
@@ -364,6 +317,7 @@ export interface InlineKeyboardElementRowButton {
enter: false enter: false
subscribeDataTemplateIds: [] subscribeDataTemplateIds: []
} }
export interface InlineKeyboardElement { export interface InlineKeyboardElement {
rows: [ rows: [
{ {
@@ -380,9 +334,9 @@ export interface TipAioOpGrayTipElement {
} }
export enum TipGroupElementType { export enum TipGroupElementType {
memberIncrease = 1, MemberIncrease = 1,
kicked = 3, // 被移出群 Kicked = 3, // 被移出群
ban = 8, Ban = 8,
} }
export interface TipGroupElement { export interface TipGroupElement {
@@ -424,17 +378,27 @@ export interface TipGroupElement {
} }
} }
export interface StructLongMsgElement {
xmlContent: string
resId: string
}
export interface MultiForwardMsgElement { export interface MultiForwardMsgElement {
xmlContent: string // xml格式的消息内容 xmlContent: string // xml格式的消息内容
resId: string resId: string
fileName: string fileName: string
} }
export enum ChatType {
C2C = 1,
Group = 2,
TempC2CFromGroup = 100,
}
export interface RawMessage { export interface RawMessage {
msgId: string msgId: string
msgType: number msgType: number
subMsgType: number subMsgType: number
msgShortId?: number // 自己维护的消息id
msgTime: string // 时间戳,秒 msgTime: string // 时间戳,秒
msgSeq: string msgSeq: string
msgRandom: string msgRandom: string
@@ -474,12 +438,11 @@ export interface MessageElement {
fileElement?: FileElement fileElement?: FileElement
liveGiftElement?: unknown liveGiftElement?: unknown
markdownElement?: MarkdownElement markdownElement?: MarkdownElement
structLongMsgElement?: unknown structLongMsgElement?: StructLongMsgElement
multiForwardMsgElement?: MultiForwardMsgElement multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: unknown giphyElement?: unknown
walletElement?: unknown
inlineKeyboardElement?: InlineKeyboardElement inlineKeyboardElement?: InlineKeyboardElement
textGiftElement?: unknown //???? textGiftElement?: unknown
calendarElement?: unknown calendarElement?: unknown
yoloGameResultElement?: unknown yoloGameResultElement?: unknown
avRecordElement?: unknown avRecordElement?: unknown
@@ -515,7 +478,7 @@ export interface OnRichMediaDownloadCompleteParams {
userUsedSpacePerDay: unknown userUsedSpacePerDay: unknown
} }
export interface OnGroupFileInfoUpdateParams { export interface GroupFileInfo {
retCode: number retCode: number
retMsg: string retMsg: string
clientWording: string clientWording: string
@@ -565,3 +528,34 @@ export interface OnGroupFileInfoUpdateParams {
nextIndex: number nextIndex: number
reqId: number reqId: number
} }
export interface QueryMsgsParams {
chatInfo: Peer
filterMsgType: []
filterSendersUid: string[]
filterMsgFromTime: string
filterMsgToTime: string
pageLimit: number
isReverseOrder: boolean
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi extends GeneralCallResult {
tmpChatInfo?: {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
}
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
folderId?: string
}

View File

@@ -56,20 +56,8 @@ export enum GroupRequestOperateTypes {
} }
export enum BuddyReqType { export enum BuddyReqType {
KMEINITIATOR, MsgInfo = 12,
KPEERINITIATOR, MeInitiatorWaitPeerConfirm = 13,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
@@ -128,4 +116,4 @@ export interface GroupExtParam {
memberIcon: number memberIcon: number
memberInfoSeq: number memberInfoSeq: number
} }
} }

View File

@@ -221,11 +221,6 @@ interface RelationFlags {
isHidePrivilegeIcon: number isHidePrivilegeIcon: number
} }
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt { interface CommonExt {
constellation: number constellation: number
shengXiao: number shengXiao: number
@@ -255,7 +250,7 @@ interface PhotoWall {
picList: Pic[] picList: Pic[]
} }
export interface UserDetailInfoListenerArg { export interface UserDetailInfo {
uid: string uid: string
uin: string uin: string
simpleInfo: SimpleInfo simpleInfo: SimpleInfo
@@ -344,3 +339,21 @@ export interface UserDetailInfoByUin {
vipNameColorId: string vipNameColorId: string
} }
} }
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}

View File

@@ -48,7 +48,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
peerUid: fileCache[0].peerUid, peerUid: fileCache[0].peerUid,
guildId: '' guildId: ''
} }
if (fileCache[0].elementType === ElementType.PIC) { if (fileCache[0].elementType === ElementType.Pic) {
const msgList = await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId]) const msgList = await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (msgList.msgList.length === 0) { if (msgList.msgList.length === 0) {
throw new Error('msg not found') throw new Error('msg not found')
@@ -59,7 +59,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
throw new Error('element not found') throw new Error('element not found')
} }
res.url = await this.ctx.ntFileApi.getImageUrl(findEle.picElement!) res.url = await this.ctx.ntFileApi.getImageUrl(findEle.picElement!)
} else if (fileCache[0].elementType === ElementType.VIDEO) { } else if (fileCache[0].elementType === ElementType.Video) {
res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId) res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
} }
if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) { if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {

View File

@@ -34,25 +34,23 @@ export class GetForwardMsg extends BaseAction<Payload, Response> {
if (data?.result !== 0) { if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg) throw Error('找不到相关的聊天记录' + data?.errMsg)
} }
const msgList = data.msgList const messages: (OB11ForwardMessage | undefined)[] = await Promise.all(
const messages = await Promise.all( data.msgList.map(async (msg) => {
msgList.map(async (msg) => { const res = await OB11Entities.message(this.ctx, msg)
const resMsg = await OB11Entities.message(this.ctx, msg) if (res) {
if (!resMsg) return return {
resMsg.message_id = this.ctx.store.createMsgShortId({ content: res.message,
chatType: msg.chatType, sender: {
peerUid: msg.peerUid, nickname: res.sender.nickname,
}, msg.msgId) user_id: res.sender.user_id
return resMsg },
time: res.time,
message_format: res.message_format,
message_type: res.message_type
}
}
}) })
) )
const forwardMessages = filterNullable(messages) return { messages: filterNullable(messages) }
.map(v => {
const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message
return msg as OB11ForwardMessage
})
return { messages: forwardMessages }
} }
} }

View File

@@ -26,7 +26,7 @@ export class GetEssenceMsgList extends BaseAction<Payload, EssenceMsg[]> {
const groupCode = payload.group_id.toString() const groupCode = payload.group_id.toString()
const peer = { const peer = {
guildId: '', guildId: '',
chatType: ChatType.group, chatType: ChatType.Group,
peerUid: groupCode peerUid: groupCode
} }
const essence = await this.ctx.ntGroupApi.queryCachedEssenceMsg(groupCode) const essence = await this.ctx.ntGroupApi.queryCachedEssenceMsg(groupCode)

View File

@@ -0,0 +1,82 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { pathToFileURL } from 'node:url'
import { ChatType } from '@/ntqqapi/types'
import { GroupFileInfo } from '@/ntqqapi/types'
export interface Payload {
group_id: number | string
file_id: string
busid?: number
}
export interface Response {
url: string
}
export class GetGroupFileUrl extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFileUrl
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_id: Schema.string().required()
})
protected async _handle(payload: Payload) {
const file = await this.ctx.store.getFileCacheById(payload.file_id)
if (file.length > 0) {
const { msgId, chatType, peerUid, elementId } = file[0]
const path = await this.ctx.ntFileApi.downloadMedia(msgId, chatType, peerUid, elementId)
return {
url: pathToFileURL(path).href
}
} else {
const groupId = payload.group_id.toString()
const modelId = await this.search(groupId, payload.file_id)
if (modelId) {
const peer = {
chatType: ChatType.Group,
peerUid: groupId,
guildId: ''
}
const path = await this.ctx.ntFileApi.downloadFileForModelId(peer, modelId)
return {
url: pathToFileURL(path).href
}
}
throw new Error('file not found')
}
}
private async search(groupId: string, fileId: string, folderId?: string) {
let modelId: string | undefined
let nextIndex: number | undefined
let folders: GroupFileInfo['item'] = []
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId
})
const file = res.item.find(item => item.fileInfo?.fileId === fileId)
if (file) {
modelId = file.fileInfo?.fileModelId
break
}
folders.push(...res.item.filter(item => item.folderInfo?.totalFileCount))
nextIndex = res.nextIndex
}
if (!modelId) {
for (const item of folders) {
const res = await this.search(groupId, fileId, item.folderInfo?.folderId)
if (res) {
modelId = res
break
}
}
}
return modelId
}
}

View File

@@ -1,11 +1,11 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types' import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types'
import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload { interface Payload {
group_id: string | number group_id: number | string
folder_id: string folder_id: string
file_count: string | number
} }
interface Response { interface Response {
@@ -17,19 +17,27 @@ export class GetGroupFilesByFolder extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder
payloadSchema = Schema.object({ payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(), group_id: Schema.union([Number, String]).required(),
folder_id: Schema.string().required(), folder_id: Schema.string().required()
file_count: Schema.union([Number, String]).default(50)
}) })
async _handle(payload: Payload) { async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), { const groupId = payload.group_id.toString()
sortType: 1, const data: GroupFileInfo['item'] = []
fileCount: +payload.file_count,
startIndex: 0, let nextIndex: number | undefined
sortOrder: 2, while (nextIndex !== 0) {
showOnlinedocFolder: 0, const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
folderId: payload.folder_id sortType: 1,
}) fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId: payload.folder_id
})
data.push(...res.item)
nextIndex = res.nextIndex
}
return { return {
files: data.filter(item => item.fileInfo) files: data.filter(item => item.fileInfo)
.map(item => { .map(item => {

View File

@@ -4,7 +4,7 @@ import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { RawMessage } from '@/ntqqapi/types' import { RawMessage } from '@/ntqqapi/types'
import { filterNullable } from '@/common/utils/misc' import { filterNullable, parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -23,15 +23,14 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
group_id: Schema.union([Number, String]).required(), group_id: Schema.union([Number, String]).required(),
message_seq: Schema.union([Number, String]), message_seq: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20), count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false), reverseOrder: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
}) })
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const { count, reverseOrder } = payload const { count, reverseOrder } = payload
const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() } const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() }
let msgList: RawMessage[] | undefined let msgList: RawMessage[]
// 包含 message_seq 0 if (!payload.message_seq || payload.message_seq === '0') {
if (!payload.message_seq) {
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else { } else {
const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
@@ -40,12 +39,7 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
} }
if (!msgList?.length) throw new Error('未找到消息') if (!msgList?.length) throw new Error('未找到消息')
if (reverseOrder) msgList.reverse() if (reverseOrder) msgList.reverse()
await Promise.all( const ob11MsgList = await Promise.all(msgList.map(msg => OB11Entities.message(this.ctx, msg)))
msgList.map(async 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) } return { messages: filterNullable(ob11MsgList) }
} }
} }

View File

@@ -0,0 +1,48 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Notice {
sender_id: number
publish_time: number
message: {
text: string
images: {
height: string
width: string
id: string
}[]
}
}
export class GetGroupNotice extends BaseAction<Payload, Notice[]> {
actionName = ActionName.GoCQHTTP_GetGroupNotice
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupBulletinList(payload.group_id.toString())
const result: Notice[] = []
for (const feed of data.result.feeds) {
result.push({
sender_id: +feed.uin,
publish_time: +feed.publishTime,
message: {
text: feed.msg.text,
images: feed.msg.pics.map(image => {
return {
height: String(image.height),
width: String(image.width),
id: image.id
}
})
}
})
}
return result
}
}

View File

@@ -1,10 +1,10 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types' import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload { interface Payload {
group_id: string | number group_id: number | string
file_count: string | number
} }
interface Response { interface Response {
@@ -15,18 +15,26 @@ interface Response {
export class GetGroupRootFiles extends BaseAction<Payload, Response> { export class GetGroupRootFiles extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupRootFiles actionName = ActionName.GoCQHTTP_GetGroupRootFiles
payloadSchema = Schema.object({ payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(), group_id: Schema.union([Number, String]).required()
file_count: Schema.union([Number, String]).default(50),
}) })
async _handle(payload: Payload) { async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), { const groupId = payload.group_id.toString()
sortType: 1, const data: GroupFileInfo['item'] = []
fileCount: +payload.file_count,
startIndex: 0, let nextIndex: number | undefined
sortOrder: 2, while (nextIndex !== 0) {
showOnlinedocFolder: 0, const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
}) sortType: 1,
fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
data.push(...res.item)
nextIndex = res.nextIndex
}
return { return {
files: data.filter(item => item.fileInfo) files: data.filter(item => item.fileInfo)
.map(item => { .map(item => {

View File

@@ -10,7 +10,8 @@ import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePee
interface Payload { interface Payload {
user_id?: string | number user_id?: string | number
group_id?: string | number group_id?: string | number
messages: OB11MessageNode[] messages?: OB11MessageNode[]
message?: OB11MessageNode[]
message_type?: 'group' | 'private' message_type?: 'group' | 'private'
} }
@@ -20,15 +21,20 @@ interface Response {
} }
export class SendForwardMsg extends BaseAction<Payload, Response> { export class SendForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_SendForwardMsg actionName = ActionName.SendForwardMsg
payloadSchema = Schema.object({ payloadSchema = Schema.object({
user_id: Schema.union([Number, String]), user_id: Schema.union([Number, String]),
group_id: Schema.union([Number, String]), group_id: Schema.union([Number, String]),
messages: Schema.array(Schema.any()).required(), messages: Schema.array(Schema.any()),
message: Schema.array(Schema.any()),
message_type: Schema.union(['group', 'private']) message_type: Schema.union(['group', 'private'])
}) })
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const messages = payload.messages ?? payload.message
if (!messages) {
throw new Error('未指定消息内容')
}
let contextMode = CreatePeerMode.Normal let contextMode = CreatePeerMode.Normal
if (payload.message_type === 'group') { if (payload.message_type === 'group') {
contextMode = CreatePeerMode.Group contextMode = CreatePeerMode.Group
@@ -36,8 +42,9 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
contextMode = CreatePeerMode.Private contextMode = CreatePeerMode.Private
} }
const peer = await createPeer(this.ctx, payload, contextMode) const peer = await createPeer(this.ctx, payload, contextMode)
const returnMsg = await this.handleForwardNode(peer, payload.messages) const msg = await this.handleForwardNode(peer, messages)
return { message_id: returnMsg.msgShortId! } const msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
return { message_id: msgShortId }
} }
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> { private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
@@ -52,7 +59,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
this.ctx.logger.info('克隆消息', sendElements) this.ctx.logger.info('克隆消息', sendElements)
try { try {
const peer = { const peer = {
chatType: ChatType.friend, chatType: ChatType.C2C,
peerUid: selfInfo.uid peerUid: selfInfo.uid
} }
const nodeMsg = await this.ctx.ntMsgApi.sendMsg(peer, sendElements) const nodeMsg = await this.ctx.ntMsgApi.sendMsg(peer, sendElements)
@@ -66,7 +73,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
// 返回一个合并转发的消息id // 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = { const selfPeer = {
chatType: ChatType.friend, chatType: ChatType.C2C,
peerUid: selfInfo.uid, peerUid: selfInfo.uid,
} }
const nodeMsgIds: { msgId: string, peer: Peer }[] = [] const nodeMsgIds: { msgId: string, peer: Peer }[] = []
@@ -100,7 +107,7 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
sendElementsSplit[splitIndex] = [] sendElementsSplit[splitIndex] = []
} }
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) { if (ele.elementType === ElementType.File || ele.elementType === ElementType.Video) {
if (sendElementsSplit[splitIndex].length > 0) { if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++ splitIndex++
} }
@@ -157,7 +164,6 @@ export class SendForwardMsg extends BaseAction<Payload, Response> {
throw Error('转发消息失败,节点为空') throw Error('转发消息失败,节点为空')
} }
const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds) const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
returnMsg.msgShortId = this.ctx.store.createMsgShortId(destPeer, returnMsg.msgId)
return returnMsg return returnMsg
} }
} }

View File

@@ -1,6 +1,6 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } from '@/ntqqapi/entities'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
@@ -27,7 +27,7 @@ export class UploadGroupFile extends BaseAction<Payload, null> {
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const file = await SendElementEntities.file(this.ctx, path, payload.name || fileName, payload.folder ?? payload.folder_id) const file = await SendElement.file(this.ctx, path, payload.name || fileName, payload.folder ?? payload.folder_id)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Group)
await sendMsg(this.ctx, peer, [file], []) await sendMsg(this.ctx, peer, [file], [])
return null return null

View File

@@ -1,6 +1,6 @@
import { BaseAction, Schema } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } from '@/ntqqapi/entities'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage' import { sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
@@ -23,7 +23,7 @@ export class UploadPrivateFile extends BaseAction<UploadPrivateFilePayload, null
if (!success) { if (!success) {
throw new Error(errMsg) throw new Error(errMsg)
} }
const sendFileEle = await SendElementEntities.file(this.ctx, path, payload.name || fileName) const sendFileEle = await SendElement.file(this.ctx, path, payload.name || fileName)
const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private) const peer = await createPeer(this.ctx, payload, CreatePeerMode.Private)
await sendMsg(this.ctx, peer, [sendFileEle], []) await sendMsg(this.ctx, peer, [sendFileEle], [])
return null return null

View File

@@ -1,22 +1,26 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { GroupRequestOperateTypes } from '@/ntqqapi/types' import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
flag: string flag: string
approve?: boolean | string approve: boolean
reason?: string reason?: string
} }
export default class SetGroupAddRequest extends BaseAction<Payload, null> { export default class SetGroupAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAddRequest actionName = ActionName.SetGroupAddRequest
payloadSchema = Schema.object({
flag: Schema.string().required(),
approve: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(true),
reason: Schema.string()
})
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const flag = payload.flag.toString()
const approve = payload.approve?.toString() !== 'false'
await this.ctx.ntGroupApi.handleGroupRequest( await this.ctx.ntGroupApi.handleGroupRequest(
flag, payload.flag,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason payload.reason
) )
return null return null

View File

@@ -1,26 +1,31 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { GroupMemberRole } from '@/ntqqapi/types' import { GroupMemberRole } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number group_id: number | string
user_id: number user_id: number | string
enable: boolean enable: boolean
} }
export default class SetGroupAdmin extends BaseAction<Payload, null> { export default class SetGroupAdmin extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAdmin actionName = ActionName.SetGroupAdmin
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(),
enable: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(true)
})
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) const groupCode = payload.group_id.toString()
const enable = payload.enable.toString() === 'true' const uin = payload.user_id.toString()
if (!member) { const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode)
throw `群成员${payload.user_id}不存在` if (!uid) throw new Error('无法获取用户信息')
}
await this.ctx.ntGroupApi.setMemberRole( await this.ctx.ntGroupApi.setMemberRole(
payload.group_id.toString(), groupCode,
member.uid, uid,
enable ? GroupMemberRole.admin : GroupMemberRole.normal, payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal
) )
return null return null
} }

View File

@@ -1,22 +1,27 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: number group_id: number | string
user_id: number user_id: number | string
duration: number duration: number | string
} }
export default class SetGroupBan extends BaseAction<Payload, null> { export default class SetGroupBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupBan actionName = ActionName.SetGroupBan
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(),
duration: Schema.union([Number, String]).default(30 * 60)
})
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) const groupCode = payload.group_id.toString()
if (!member) { const uin = payload.user_id.toString()
throw `群成员${payload.user_id}不存在` const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode)
} if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntGroupApi.banMember(payload.group_id.toString(), [ await this.ctx.ntGroupApi.banMember(groupCode, [
{ uid: member.uid, timeStamp: parseInt(payload.duration.toString()) }, { uid, timeStamp: +payload.duration },
]) ])
return null return null
} }

View File

@@ -1,21 +1,26 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: number group_id: number | string
user_id: number user_id: number | string
card: string card: string
} }
export default class SetGroupCard extends BaseAction<Payload, null> { export default class SetGroupCard extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupCard actionName = ActionName.SetGroupCard
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(),
card: Schema.string().default('')
})
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) const groupCode = payload.group_id.toString()
if (!member) { const uin = payload.user_id.toString()
throw `群成员${payload.user_id}不存在` const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode)
} if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || '') await this.ctx.ntGroupApi.setMemberCard(groupCode, uid, payload.card)
return null return null
} }
} }

View File

@@ -1,21 +1,27 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number group_id: number | string
user_id: number user_id: number | string
reject_add_request: boolean reject_add_request: boolean
} }
export default class SetGroupKick extends BaseAction<Payload, null> { export default class SetGroupKick extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupKick actionName = ActionName.SetGroupKick
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
user_id: Schema.union([Number, String]).required(),
reject_add_request: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
})
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const member = await this.ctx.ntGroupApi.getGroupMember(payload.group_id, payload.user_id) const groupCode = payload.group_id.toString()
if (!member) { const uin = payload.user_id.toString()
throw `群成员${payload.user_id}不存在` const uid = await this.ctx.ntUserApi.getUidByUin(uin, groupCode)
} if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request) await this.ctx.ntGroupApi.kickMember(groupCode, [uid], payload.reject_add_request)
return null return null
} }
} }

View File

@@ -1,20 +1,19 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: number group_id: number | string
is_dismiss: boolean is_dismiss?: boolean
} }
export default class SetGroupLeave extends BaseAction<Payload, void> { export default class SetGroupLeave extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupLeave actionName = ActionName.SetGroupLeave
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
try { await this.ctx.ntGroupApi.quitGroup(payload.group_id.toString())
await this.ctx.ntGroupApi.quitGroup(payload.group_id.toString()) return null
} catch (e) {
this.ctx.logger.error('退群失败', e)
throw e
}
} }
} }

View File

@@ -1,13 +1,17 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Payload { interface Payload {
group_id: number group_id: number | string
group_name: string group_name: string
} }
export default class SetGroupName extends BaseAction<Payload, null> { export default class SetGroupName extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupName actionName = ActionName.SetGroupName
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
group_name: Schema.string().required()
})
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
await this.ctx.ntGroupApi.setGroupName(payload.group_id.toString(), payload.group_name) await this.ctx.ntGroupApi.setGroupName(payload.group_id.toString(), payload.group_name)

View File

@@ -1,17 +1,21 @@
import { BaseAction } from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number group_id: number | string
enable: boolean enable: boolean
} }
export default class SetGroupWholeBan extends BaseAction<Payload, null> { export default class SetGroupWholeBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupWholeBan actionName = ActionName.SetGroupWholeBan
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
enable: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(true)
})
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const enable = payload.enable.toString() === 'true' await this.ctx.ntGroupApi.banGroup(payload.group_id.toString(), payload.enable)
await this.ctx.ntGroupApi.banGroup(payload.group_id.toString(), enable)
return null return null
} }
} }

View File

@@ -45,7 +45,7 @@ import { GetGroupMsgHistory } from './go-cqhttp/GetGroupMsgHistory'
import GetFile from './file/GetFile' import GetFile from './file/GetFile'
import { GetForwardMsg } from './go-cqhttp/GetForwardMsg' import { GetForwardMsg } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie' import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike' import { SetMsgEmojiLike } from './llonebot/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg' import { ForwardFriendSingleMsg, ForwardGroupSingleMsg } from './msg/ForwardSingleMsg'
import { GetEssenceMsgList } from './go-cqhttp/GetGroupEssence' import { GetEssenceMsgList } from './go-cqhttp/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo' import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
@@ -69,6 +69,9 @@ import { GetGroupFilesByFolder } from './go-cqhttp/GetGroupFilesByFolder'
import { GetFriendWithCategory } from './llonebot/GetFriendWithCategory' import { GetFriendWithCategory } from './llonebot/GetFriendWithCategory'
import { UploadGroupFile } from './go-cqhttp/UploadGroupFile' import { UploadGroupFile } from './go-cqhttp/UploadGroupFile'
import { UploadPrivateFile } from './go-cqhttp/UploadPrivateFile' import { UploadPrivateFile } from './go-cqhttp/UploadPrivateFile'
import { GetGroupFileUrl } from './go-cqhttp/GetGroupFileUrl'
import { GetGroupNotice } from './go-cqhttp/GetGroupNotice'
import { GetRobotUinRange } from './llonebot/GetRobotUinRange'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -85,6 +88,8 @@ export function initActionMap(adapter: Adapter) {
new GetFriendMsgHistory(adapter), new GetFriendMsgHistory(adapter),
new FetchEmojiLike(adapter), new FetchEmojiLike(adapter),
new FetchCustomFace(adapter), new FetchCustomFace(adapter),
new SetMsgEmojiLike(adapter),
new GetRobotUinRange(adapter),
// onebot11 // onebot11
new SendLike(adapter), new SendLike(adapter),
new GetMsg(adapter), new GetMsg(adapter),
@@ -115,7 +120,6 @@ export function initActionMap(adapter: Adapter) {
new GetRecord(adapter), new GetRecord(adapter),
new CleanCache(adapter), new CleanCache(adapter),
new GetCookies(adapter), new GetCookies(adapter),
new SetMsgEmojiLike(adapter),
new ForwardFriendSingleMsg(adapter), new ForwardFriendSingleMsg(adapter),
new ForwardGroupSingleMsg(adapter), new ForwardGroupSingleMsg(adapter),
// go-cqhttp // go-cqhttp
@@ -143,6 +147,8 @@ export function initActionMap(adapter: Adapter) {
new GetGroupRootFiles(adapter), new GetGroupRootFiles(adapter),
new SendGroupNotice(adapter), new SendGroupNotice(adapter),
new GetGroupFilesByFolder(adapter), new GetGroupFilesByFolder(adapter),
new GetGroupFileUrl(adapter),
new GetGroupNotice(adapter),
] ]
const actionMap = new Map<string, BaseAction<any, unknown>>() const actionMap = new Map<string, BaseAction<any, unknown>>()
for (const action of actionHandlers) { for (const action of actionHandlers) {

View File

@@ -3,7 +3,7 @@ import { OB11Message } from '@/onebot11/types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType, RawMessage } from '@/ntqqapi/types' import { ChatType, RawMessage } from '@/ntqqapi/types'
import { OB11Entities } from '@/onebot11/entities' import { OB11Entities } from '@/onebot11/entities'
import { filterNullable } from '@/common/utils/misc' import { filterNullable, parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
user_id: number | string user_id: number | string
@@ -24,7 +24,7 @@ export class GetFriendMsgHistory extends BaseAction<Payload, Response> {
message_seq: Schema.union([Number, String]), message_seq: Schema.union([Number, String]),
message_id: Schema.union([Number, String]), message_id: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20), count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false) reverseOrder: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
}) })
async _handle(payload: Payload): Promise<Response> { async _handle(payload: Payload): Promise<Response> {
@@ -38,14 +38,11 @@ export class GetFriendMsgHistory extends BaseAction<Payload, Response> {
const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString()) const uid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!uid) throw new Error(`记录${payload.user_id}不存在`) if (!uid) throw new Error(`记录${payload.user_id}不存在`)
const isBuddy = await this.ctx.ntFriendApi.isBuddy(uid) const isBuddy = await this.ctx.ntFriendApi.isBuddy(uid)
const peer = { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid: uid } const peer = { chatType: isBuddy ? ChatType.C2C : ChatType.TempC2CFromGroup, peerUid: uid }
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList
} }
if (msgList.length === 0) throw new Error('未找到消息') if (msgList.length === 0) throw new Error('未找到消息')
if (payload.reverseOrder) msgList.reverse() if (payload.reverseOrder) msgList.reverse()
for (const msg of msgList) {
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))) const ob11MsgList = await Promise.all(msgList.map(msg => OB11Entities.message(this.ctx, msg)))
return { messages: filterNullable(ob11MsgList) } return { messages: filterNullable(ob11MsgList) }
} }

View File

@@ -4,14 +4,35 @@ import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
export class GetFriendWithCategory extends BaseAction<void, OB11User[]> { interface Category {
categoryId: number
categorySortId: number
categoryName: string
categoryMbCount: number
onlineCount: number
buddyList: OB11User[]
}
export class GetFriendWithCategory extends BaseAction<void, Category[]> {
actionName = ActionName.GetFriendsWithCategory actionName = ActionName.GetFriendsWithCategory
protected async _handle() { protected async _handle() {
if (getBuildVersion() >= 26702) { if (getBuildVersion() < 26702) {
return OB11Entities.friendsV2(await this.ctx.ntFriendApi.getBuddyV2ExWithCate(true))
} else {
throw new Error('this ntqq version not support, must be 26702 or later') throw new Error('this ntqq version not support, must be 26702 or later')
} }
const data = await this.ctx.ntFriendApi.getBuddyV2WithCate(true)
return data.buddyCategory.map(item => {
return {
categoryId: item.categoryId,
categorySortId: item.categorySortId,
categoryName: item.categroyName,
categoryMbCount: item.categroyMbCount,
onlineCount: item.onlineCount,
buddyList: item.buddyUids.map(uid => {
const info = data.userSimpleInfos[uid]
return OB11Entities.friendV2(info)
})
}
})
} }
} }

View File

@@ -0,0 +1,11 @@
import { BaseAction } from '../BaseAction'
import { ActionName } from '../types'
import { Dict } from 'cosmokit'
export class GetRobotUinRange extends BaseAction<void, Dict[]> {
actionName = ActionName.GetRobotUinRange
async _handle() {
return await this.ctx.ntUserApi.getRobotUinRange()
}
}

View File

@@ -1,7 +1,6 @@
import { BaseAction } from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ChatType } from '@/ntqqapi/types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types' import { createPeer } from '@/onebot11/helper/createMessage'
interface Payload { interface Payload {
message_id: number | string message_id: number | string
@@ -10,17 +9,6 @@ interface Payload {
} }
abstract class ForwardSingleMsg extends BaseAction<Payload, null> { abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await this.ctx.ntUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw new Error(`无法找到私聊对象${payload.user_id}`)
}
return { chatType: ChatType.friend, peerUid }
}
return { chatType: ChatType.group, peerUid: payload.group_id!.toString() }
}
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
if (!payload.message_id) { if (!payload.message_id) {
throw Error('message_id不能为空') throw Error('message_id不能为空')
@@ -29,10 +17,10 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
if (!msg) { if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`) throw new Error(`无法找到消息${payload.message_id}`)
} }
const peer = await this.getTargetPeer(payload) const peer = await createPeer(this.ctx, 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) { if (ret.length === 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`) throw new Error(`转发消息失败`)
} }
return null return null
} }

View File

@@ -25,14 +25,11 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
peerUid: msgInfo.peer.peerUid, peerUid: msgInfo.peer.peerUid,
chatType: msgInfo.peer.chatType chatType: msgInfo.peer.chatType
} }
const msg = this.adapter.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0] const msg = this.ctx.store.getMsgCache(msgInfo.msgId) ?? (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgInfo.msgId])).msgList[0]
const retMsg = await OB11Entities.message(this.ctx, msg) const retMsg = await OB11Entities.message(this.ctx, msg)
if (!retMsg) { if (!retMsg) {
throw new Error('消息为空') throw new Error('消息为空')
} }
retMsg.message_id = this.ctx.store.createMsgShortId(peer, msg.msgId)
retMsg.message_seq = retMsg.message_id
retMsg.real_id = retMsg.message_id
return retMsg return retMsg
} }
} }

View File

@@ -93,7 +93,11 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
if (!returnMsg) { if (!returnMsg) {
throw new Error('消息发送失败') throw new Error('消息发送失败')
} }
return { message_id: returnMsg.msgShortId! } const msgShortId = this.ctx.store.createMsgShortId({
chatType: returnMsg.chatType,
peerUid: returnMsg.peerUid
}, returnMsg.msgId)
return { message_id: msgShortId }
} }
private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number { private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number {

View File

@@ -24,6 +24,9 @@ export enum ActionName {
FetchEmojiLike = 'fetch_emoji_like', FetchEmojiLike = 'fetch_emoji_like',
FetchCustomFace = 'fetch_custom_face', FetchCustomFace = 'fetch_custom_face',
GetFriendMsgHistory = 'get_friend_msg_history', GetFriendMsgHistory = 'get_friend_msg_history',
SendForwardMsg = 'send_forward_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
GetRobotUinRange = 'get_robot_uin_range',
// onebot 11 // onebot 11
SendLike = 'send_like', SendLike = 'send_like',
GetLoginInfo = 'get_login_info', GetLoginInfo = 'get_login_info',
@@ -37,7 +40,6 @@ export enum ActionName {
SendGroupMsg = 'send_group_msg', SendGroupMsg = 'send_group_msg',
SendPrivateMsg = 'send_private_msg', SendPrivateMsg = 'send_private_msg',
DeleteMsg = 'delete_msg', DeleteMsg = 'delete_msg',
SetMsgEmojiLike = 'set_msg_emoji_like',
SetGroupAddRequest = 'set_group_add_request', SetGroupAddRequest = 'set_group_add_request',
SetFriendAddRequest = 'set_friend_add_request', SetFriendAddRequest = 'set_friend_add_request',
SetGroupLeave = 'set_group_leave', SetGroupLeave = 'set_group_leave',
@@ -57,8 +59,7 @@ export enum ActionName {
GetCookies = 'get_cookies', GetCookies = 'get_cookies',
ForwardFriendSingleMsg = 'forward_friend_single_msg', ForwardFriendSingleMsg = 'forward_friend_single_msg',
ForwardGroupSingleMsg = 'forward_group_single_msg', ForwardGroupSingleMsg = 'forward_group_single_msg',
// 以下为go-cqhttp api // go-cqhttp
GoCQHTTP_SendForwardMsg = 'send_forward_msg',
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg', GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg', GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
GoCQHTTP_GetStrangerInfo = 'get_stranger_info', GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
@@ -81,5 +82,7 @@ export enum ActionName {
GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain', GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
GoCQHTTP_GetGroupRootFiles = 'get_group_root_files', GoCQHTTP_GetGroupRootFiles = 'get_group_root_files',
GoCQHTTP_SendGroupNotice = '_send_group_notice', GoCQHTTP_SendGroupNotice = '_send_group_notice',
GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder' GoCQHTTP_GetGroupFilesByFolder = 'get_group_files_by_folder',
GoCQHTTP_GetGroupFileUrl = 'get_group_file_url',
GoCQHTTP_GetGroupNotice = '_get_group_notice',
} }

View File

@@ -3,6 +3,7 @@ import { OB11User } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils' import { getBuildVersion } from '@/common/utils'
import { parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
no_cache: boolean no_cache: boolean
@@ -11,7 +12,7 @@ interface Payload {
export class GetFriendList extends BaseAction<Payload, OB11User[]> { export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList actionName = ActionName.GetFriendList
payloadSchema = Schema.object({ payloadSchema = Schema.object({
no_cache: Schema.boolean().default(false) no_cache: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
}) })
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {

View File

@@ -1,5 +1,6 @@
import { BaseAction } from '../BaseAction' import { BaseAction } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types'
interface Payload { interface Payload {
flag: string flag: string
@@ -22,6 +23,11 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
if (payload.remark) { if (payload.remark) {
await this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark) await this.ctx.ntFriendApi.setBuddyRemark(uid, payload.remark)
} }
await this.ctx.ntMsgApi.activateChat({
peerUid: uid,
chatType: ChatType.C2C,
guildId: ''
})
return null return null
} }
} }

View File

@@ -5,7 +5,6 @@ import {
GroupNotifyType, GroupNotifyType,
RawMessage, RawMessage,
BuddyReqType, BuddyReqType,
Peer,
FriendRequest, FriendRequest,
GroupMember, GroupMember,
GroupMemberRole, GroupMemberRole,
@@ -38,8 +37,6 @@ declare module 'cordis' {
class OneBot11Adapter extends Service { class OneBot11Adapter extends Service {
static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store'] static inject = ['ntMsgApi', 'ntFileApi', 'ntFileCacheApi', 'ntFriendApi', 'ntGroupApi', 'ntUserApi', 'ntWindowApi', 'ntWebApi', 'store']
public messages: Map<string, RawMessage> = new Map()
public startTime = 0
private ob11WebSocket: OB11WebSocket private ob11WebSocket: OB11WebSocket
private ob11WebSocketReverseManager: OB11WebSocketReverseManager private ob11WebSocketReverseManager: OB11WebSocketReverseManager
private ob11Http: OB11Http private ob11Http: OB11Http
@@ -75,24 +72,6 @@ class OneBot11Adapter extends Service {
}) })
} }
/** 缓存近期消息内容 */
public async addMsgCache(msg: RawMessage) {
const expire = this.config.msgCacheExpire * 1000
if (expire === 0) {
return
}
const id = msg.msgId
this.messages.set(id, msg)
setTimeout(() => {
this.messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
public getMsgCache(msgId: string) {
return this.messages.get(msgId)
}
public dispatch(event: OB11BaseEvent | OB11Message) { public dispatch(event: OB11BaseEvent | OB11Message) {
if (this.config.enableWs) { if (this.config.enableWs) {
this.ob11WebSocket.emitEvent(event) this.ob11WebSocket.emitEvent(event)
@@ -109,120 +88,99 @@ class OneBot11Adapter extends Service {
} }
} }
private async handleGroupNotify(notifies: GroupNotify[]) { private async handleGroupNotify(notify: GroupNotify) {
for (const notify of notifies) { try {
try { const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
const notifyTime = parseInt(notify.seq) / 1000 if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) {
if (notifyTime < this.startTime) { this.ctx.logger.info('有成员退出通知', notify)
continue const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
} let operatorId = member1Uin
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type let subType: GroupDecreaseSubType = 'leave'
if ([GroupNotifyType.MEMBER_LEAVE_NOTIFY_ADMIN, GroupNotifyType.KICK_MEMBER_NOTIFY_ADMIN].includes(notify.type)) { if (notify.user2.uid) {
this.ctx.logger.info('有成员退出通知', notify) // 是被踢的
const member1Uin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) const member2Uin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
let operatorId = member1Uin if (member2Uin) {
let subType: GroupDecreaseSubType = 'leave' operatorId = member2Uin
if (notify.user2.uid) {
// 是被踢的
const member2Uin = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
if (member2Uin) {
operatorId = member2Uin
}
subType = 'kick'
} }
const event = new OB11GroupDecreaseEvent( subType = 'kick'
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
)
this.dispatch(event)
} }
else if (notify.type === GroupNotifyType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) { const event = new OB11GroupDecreaseEvent(
this.ctx.logger.info('有加群请求') parseInt(notify.group.groupCode),
const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid) parseInt(member1Uin),
const event = new OB11GroupRequestEvent( parseInt(operatorId),
parseInt(notify.group.groupCode), subType,
parseInt(requestUin) || 0, )
flag, this.dispatch(event)
notify.postscript,
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_BY_MEMBER && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到邀请我加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript,
undefined,
'invite'
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript
)
this.dispatch(event)
}
} catch (e) {
this.ctx.logger.error('解析群通知失败', (e as Error).stack)
} }
else if (notify.type === GroupNotifyType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('有加群请求')
const requestUin = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestUin) || 0,
flag,
notify.postscript,
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_BY_MEMBER && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到邀请我加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript,
undefined,
'invite'
)
this.dispatch(event)
}
else if (notify.type === GroupNotifyType.INVITED_NEED_ADMINI_STRATOR_PASS && notify.status === GroupNotifyStatus.KUNHANDLE) {
this.ctx.logger.info('收到群员邀请加群通知')
const userId = await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)
const event = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId) || 0,
flag,
notify.postscript
)
this.dispatch(event)
}
} catch (e) {
this.ctx.logger.error('解析群通知失败', (e as Error).stack)
} }
} }
private handleMsg(msgList: RawMessage[]) { private handleMsg(message: RawMessage) {
for (const message of msgList) { OB11Entities.message(this.ctx, message).then(msg => {
// 过滤启动之前的消息 if (!msg) {
if (parseInt(message.msgTime) < this.startTime / 1000) { return
continue
} }
const peer: Peer = { if (!this.config.debug && msg.message.length === 0) {
chatType: message.chatType, return
peerUid: message.peerUid
} }
message.msgShortId = this.ctx.store.createMsgShortId(peer, message.msgId) const isSelfMsg = msg.user_id.toString() === selfInfo.uin
this.addMsgCache(message) if (isSelfMsg && !this.config.reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
this.dispatch(msg)
}).catch(e => this.ctx.logger.error('constructMessage error: ', e.stack.toString()))
OB11Entities.message(this.ctx, message) OB11Entities.groupEvent(this.ctx, message).then(groupEvent => {
.then((msg) => { if (groupEvent) {
if (!msg) { this.dispatch(groupEvent)
return }
} })
if (!this.config.debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() === selfInfo.uin
if (isSelfMsg && !this.config.reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
this.dispatch(msg)
})
.catch((e) => this.ctx.logger.error('constructMessage error: ', e.stack.toString()))
OB11Entities.groupEvent(this.ctx, message).then((groupEvent) => { OB11Entities.privateEvent(this.ctx, message).then(privateEvent => {
if (groupEvent) { if (privateEvent) {
this.dispatch(groupEvent) this.dispatch(privateEvent)
} }
}) })
OB11Entities.privateEvent(this.ctx, message).then((privateEvent) => {
if (privateEvent) {
this.dispatch(privateEvent)
}
})
}
} }
private handleRecallMsg(message: RawMessage) { private handleRecallMsg(message: RawMessage) {
@@ -241,30 +199,22 @@ class OneBot11Adapter extends Service {
}) })
} }
private async handleFriendRequest(buddyReqs: FriendRequest[]) { private async handleFriendRequest(req: FriendRequest) {
for (const req of buddyReqs) { let userId = 0
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) { try {
continue const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid)
} userId = parseInt(requesterUin)
if (+req.reqTime < this.startTime / 1000) { } catch (e) {
continue this.ctx.logger.error('获取加好友者QQ号失败', e)
}
let userId = 0
try {
const requesterUin = await this.ctx.ntUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin)
} catch (e) {
this.ctx.logger.error('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
this.dispatch(friendRequestEvent)
} }
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
this.dispatch(friendRequestEvent)
} }
private async handleConfigUpdated(config: LLOBConfig) { private async handleConfigUpdated(config: LLOBConfig) {
@@ -380,7 +330,6 @@ class OneBot11Adapter extends Service {
} }
public start() { public start() {
this.startTime = Date.now()
if (this.config.enableWs) { if (this.config.enableWs) {
this.ob11WebSocket.start() this.ob11WebSocket.start()
} }
@@ -403,7 +352,7 @@ class OneBot11Adapter extends Service {
this.handleRecallMsg(input) this.handleRecallMsg(input)
}) })
this.ctx.on('nt/message-sent', input => { this.ctx.on('nt/message-sent', input => {
this.handleMsg([input]) this.handleMsg(input)
}) })
this.ctx.on('nt/group-notify', input => { this.ctx.on('nt/group-notify', input => {
this.handleGroupNotify(input) this.handleGroupNotify(input)

View File

@@ -1,7 +1,7 @@
import http from 'node:http' import http from 'node:http'
import cors from 'cors' import cors from 'cors'
import crypto from 'node:crypto' import crypto from 'node:crypto'
import express, { Express, Request, Response } from 'express' import express, { Express, Request, Response, NextFunction } from 'express'
import { BaseAction } from '../action/BaseAction' import { BaseAction } from '../action/BaseAction'
import { Context } from 'cordis' import { Context } from 'cordis'
import { llonebotError, selfInfo } from '@/common/globalVars' import { llonebotError, selfInfo } from '@/common/globalVars'
@@ -12,8 +12,6 @@ import { handleQuickOperation, QuickOperationEvent } from '../helper/quickOperat
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
import { Dict } from 'cosmokit' import { Dict } from 'cosmokit'
type RegisterHandler = (res: Response, payload: unknown) => Promise<unknown>
class OB11Http { class OB11Http {
private readonly expressAPP: Express private readonly expressAPP: Express
private server?: http.Server private server?: http.Server
@@ -25,7 +23,6 @@ class OB11Http {
this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' })) this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' }))
this.expressAPP.use((req, res, next) => { this.expressAPP.use((req, res, next) => {
// 兼容处理没有带content-type的请求 // 兼容处理没有带content-type的请求
// log("req.headers['content-type']", req.headers['content-type'])
req.headers['content-type'] = 'application/json' req.headers['content-type'] = 'application/json'
const originalJson = express.json({ limit: '5000mb' }) const originalJson = express.json({ limit: '5000mb' })
// 调用原始的express.json()处理器 // 调用原始的express.json()处理器
@@ -37,12 +34,8 @@ class OB11Http {
next() next()
}) })
}) })
setTimeout(() => { this.expressAPP.use((req, res, next) => this.authorize(req, res, next))
for (const [actionName, action] of config.actionMap) { this.expressAPP.use((req, res, next) => this.handleRequest(req, res, next))
this.registerRouter('post', actionName, (res, payload) => action.handle(payload))
this.registerRouter('get', actionName, (res, payload) => action.handle(payload))
}
}, 0)
} }
public start() { public start() {
@@ -83,8 +76,10 @@ class OB11Http {
Object.assign(this.config, config) Object.assign(this.config, config)
} }
private authorize(req: Request, res: Response, next: () => void) { private authorize(req: Request, res: Response, next: NextFunction) {
const serverToken = this.config.token const serverToken = this.config.token
if (!serverToken) return next()
let clientToken = '' let clientToken = ''
const authHeader = req.get('authorization') const authHeader = req.get('authorization')
if (authHeader) { if (authHeader) {
@@ -99,36 +94,32 @@ class OB11Http {
this.ctx.logger.info('receive http url token', clientToken) this.ctx.logger.info('receive http url token', clientToken)
} }
if (serverToken && clientToken !== serverToken) { if (clientToken !== serverToken) {
return res.status(403).send(JSON.stringify({ message: 'token verify failed!' })) res.status(403).json({ message: 'token verify failed!' })
} else {
next()
} }
next()
} }
private registerRouter(method: 'post' | 'get', url: string, handler: RegisterHandler) { private async handleRequest(req: Request, res: Response, next: NextFunction) {
if (!url.startsWith('/')) { if (req.path === '/') return next()
url = '/' + url let payload = req.body
if (req.method === 'GET') {
payload = req.query
} else if (req.query) {
payload = { ...req.query, ...req.body }
} }
this.ctx.logger.info('收到 HTTP 请求', req.url, payload)
if (!this.expressAPP[method]) { const action = this.config.actionMap.get(req.path.replaceAll('/', ''))
const err = `LLOneBot server register router failed${method} not exist` if (action) {
this.ctx.logger.error(err)
throw err
}
this.expressAPP[method](url, this.authorize.bind(this), async (req: Request, res: Response) => {
let payload = req.body
if (method == 'get') {
payload = req.query
} else if (req.query) {
payload = { ...req.query, ...req.body }
}
this.ctx.logger.info('收到 HTTP 请求', url, payload)
try { try {
res.send(await handler(res, payload)) res.json(await action.handle(payload))
} catch (e) { } catch (e) {
res.send(OB11Response.error((e as Error).stack!.toString(), 200)) res.json(OB11Response.error((e as Error).stack!.toString(), 200))
} }
}) } else {
res.status(404).json(OB11Response.error('API 不存在', 404))
}
} }
} }

View File

@@ -72,7 +72,3 @@ export function encodeCQCode(input: OB11MessageData) {
result += ']' result += ']'
return result return result
} }
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
// const result = parseCQCode("好好好")
// console.log(JSON.stringify(result))

View File

@@ -20,7 +20,7 @@ import {
Sex, Sex,
TipGroupElementType, TipGroupElementType,
User, User,
FriendV2 SimpleInfo
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
@@ -53,14 +53,15 @@ export namespace OB11Entities {
messagePostFormat, messagePostFormat,
} = ctx.config as OneBot11Adapter.Config } = ctx.config as OneBot11Adapter.Config
const selfUin = selfInfo.uin const selfUin = selfInfo.uin
const msgShortId = ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: parseInt(selfUin), self_id: parseInt(selfUin),
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin),
time: parseInt(msg.msgTime) || Date.now(), time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId!, message_id: msgShortId,
real_id: msg.msgShortId!, real_id: msgShortId,
message_seq: msg.msgShortId!, message_seq: msgShortId,
message_type: msg.chatType === ChatType.group ? 'group' : 'private', message_type: msg.chatType === ChatType.Group ? 'group' : 'private',
sender: { sender: {
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin),
nickname: msg.sendNickName, nickname: msg.sendNickName,
@@ -76,7 +77,7 @@ export namespace OB11Entities {
if (debug) { if (debug) {
resMsg.raw = msg resMsg.raw = msg
} }
if (msg.chatType === ChatType.group) { if (msg.chatType === ChatType.Group) {
resMsg.sub_type = 'normal' resMsg.sub_type = 'normal'
resMsg.group_id = parseInt(msg.peerUin) resMsg.group_id = parseInt(msg.peerUin)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin) const member = await ctx.ntGroupApi.getGroupMember(msg.peerUin, msg.senderUin)
@@ -86,15 +87,15 @@ export namespace OB11Entities {
resMsg.sender.title = member.memberSpecialTitle ?? '' resMsg.sender.title = member.memberSpecialTitle ?? ''
} }
} }
else if (msg.chatType === ChatType.friend) { else if (msg.chatType === ChatType.C2C) {
resMsg.sub_type = 'friend' resMsg.sub_type = 'friend'
resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick
} }
else if (msg.chatType === ChatType.temp) { else if (msg.chatType === ChatType.TempC2CFromGroup) {
resMsg.sub_type = 'group' resMsg.sub_type = 'group'
resMsg.temp_source = 0 //群聊 resMsg.temp_source = 0 //群聊
resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick resMsg.sender.nickname = (await ctx.ntUserApi.getUserDetailInfo(msg.senderUid)).nick
const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType.temp, msg.senderUid) const ret = await ctx.ntMsgApi.getTempChatInfo(ChatType.TempC2CFromGroup, msg.senderUid)
if (ret?.result === 0) { if (ret?.result === 0) {
resMsg.sender.group_id = Number(ret.tmpChatInfo?.groupCode) resMsg.sender.group_id = Number(ret.tmpChatInfo?.groupCode)
} else { } else {
@@ -104,37 +105,31 @@ export namespace OB11Entities {
for (const element of msg.elements) { for (const element of msg.elements) {
let messageSegment: OB11MessageData | undefined let messageSegment: OB11MessageData | undefined
if (element.textElement && element.textElement?.atType !== AtType.notAt) { if (element.textElement && element.textElement?.atType !== AtType.Unknown) {
let qq: string let qq: string
let name: string | undefined let name: string | undefined
if (element.textElement.atType == AtType.atAll) { if (element.textElement.atType === AtType.All) {
qq = 'all' qq = 'all'
} } else {
else { const { atNtUid, atUid, content } = element.textElement
const { atNtUid, content } = element.textElement if (atUid && atUid !== '0') {
let atQQ = element.textElement.atUid qq = atUid
if (!atQQ || atQQ === '0') { } else {
const atMember = await ctx.ntGroupApi.getGroupMember(msg.peerUin, atNtUid) qq = await ctx.ntUserApi.getUinByUid(atNtUid)
if (atMember) {
atQQ = atMember.uin
}
}
if (atQQ) {
qq = atQQ
name = content.replace('@', '')
} }
name = content.replace('@', '')
} }
messageSegment = { messageSegment = {
type: OB11MessageDataType.at, type: OB11MessageDataType.at,
data: { data: {
qq: qq!, qq,
name name
} }
} }
} }
else if (element.textElement) { else if (element.textElement) {
const text = element.textElement.content const text = element.textElement.content
if (!text.trim()) { if (!text) {
continue continue
} }
messageSegment = { messageSegment = {
@@ -296,28 +291,31 @@ export namespace OB11Entities {
} }
else if (element.faceElement) { else if (element.faceElement) {
const { faceElement } = element const { faceElement } = element
const faceId = faceElement.faceIndex const { faceIndex, pokeType } = faceElement
if (faceId === FaceIndex.dice) { if (faceIndex === FaceIndex.Dice) {
messageSegment = { messageSegment = {
type: OB11MessageDataType.dice, type: OB11MessageDataType.dice,
data: { data: {
result: faceElement.resultId! result: faceElement.resultId!
} }
} }
} } else if (faceIndex === FaceIndex.RPS) {
else if (faceId === FaceIndex.RPS) {
messageSegment = { messageSegment = {
type: OB11MessageDataType.RPS, type: OB11MessageDataType.RPS,
data: { data: {
result: faceElement.resultId! result: faceElement.resultId!
} }
} }
} /*} else if (faceIndex === 1 && pokeType === 1) {
else { messageSegment = {
type: OB11MessageDataType.shake,
data: {}
}*/
} else {
messageSegment = { messageSegment = {
type: OB11MessageDataType.face, type: OB11MessageDataType.face,
data: { data: {
id: faceId.toString() id: faceIndex.toString()
} }
} }
} }
@@ -340,7 +338,6 @@ export namespace OB11Entities {
key: marketFaceElement.key key: marketFaceElement.key
} }
} }
//mFaceCache.set(emojiId, element.marketFaceElement.faceName!)
} }
else if (element.markdownElement) { else if (element.markdownElement) {
const { markdownElement } = element const { markdownElement } = element
@@ -361,20 +358,20 @@ export namespace OB11Entities {
} }
if (messageSegment) { if (messageSegment) {
const cqCode = encodeCQCode(messageSegment) const cqCode = encodeCQCode(messageSegment)
if (messagePostFormat === 'string') { if (messagePostFormat === 'array') {
(resMsg.message as string) += cqCode
} else {
(resMsg.message as OB11MessageData[]).push(messageSegment) (resMsg.message as OB11MessageData[]).push(messageSegment)
} }
resMsg.raw_message += cqCode resMsg.raw_message += cqCode
} }
} }
resMsg.raw_message = resMsg.raw_message.trim() if (messagePostFormat === 'string') {
resMsg.message = resMsg.raw_message
}
return resMsg return resMsg
} }
export async function privateEvent(ctx: Context, msg: RawMessage): Promise<OB11BaseNoticeEvent | void> { export async function privateEvent(ctx: Context, msg: RawMessage): Promise<OB11BaseNoticeEvent | void> {
if (msg.chatType !== ChatType.friend) { if (msg.chatType !== ChatType.C2C) {
return return
} }
for (const element of msg.elements) { for (const element of msg.elements) {
@@ -403,7 +400,7 @@ export namespace OB11Entities {
} }
export async function groupEvent(ctx: Context, msg: RawMessage): Promise<OB11GroupNoticeEvent | void> { export async function groupEvent(ctx: Context, msg: RawMessage): Promise<OB11GroupNoticeEvent | void> {
if (msg.chatType !== ChatType.group) { if (msg.chatType !== ChatType.Group) {
return return
} }
if (msg.senderUin) { if (msg.senderUin) {
@@ -423,7 +420,7 @@ export namespace OB11Entities {
const grayTipElement = element.grayTipElement const grayTipElement = element.grayTipElement
const groupElement = grayTipElement?.groupElement const groupElement = grayTipElement?.groupElement
if (groupElement) { if (groupElement) {
if (groupElement.type === TipGroupElementType.memberIncrease) { if (groupElement.type === TipGroupElementType.MemberIncrease) {
ctx.logger.info('收到群成员增加消息', groupElement) ctx.logger.info('收到群成员增加消息', groupElement)
await ctx.sleep(1000) await ctx.sleep(1000)
const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid) const member = await ctx.ntGroupApi.getGroupMember(msg.peerUid, groupElement.memberUid)
@@ -437,8 +434,8 @@ export namespace OB11Entities {
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin)) return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin))
} }
} }
else if (groupElement.type === TipGroupElementType.ban) { else if (groupElement.type === TipGroupElementType.Ban) {
ctx.logger.info('收到群员禁言提示', groupElement) ctx.logger.info('收到群员禁言提示', groupElement)
const memberUid = groupElement.shutUp?.member.uid const memberUid = groupElement.shutUp?.member.uid
const adminUid = groupElement.shutUp?.admin.uid const adminUid = groupElement.shutUp?.admin.uid
let memberUin: string = '' let memberUin: string = ''
@@ -467,7 +464,7 @@ export namespace OB11Entities {
) )
} }
} }
else if (groupElement.type === TipGroupElementType.kicked) { else if (groupElement.type === TipGroupElementType.Kicked) {
ctx.logger.info(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) ctx.logger.info(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
ctx.ntGroupApi.quitGroup(msg.peerUid) ctx.ntGroupApi.quitGroup(msg.peerUid)
try { try {
@@ -513,7 +510,7 @@ export namespace OB11Entities {
const msgSeq: string = emojiLikeData.gtip.url.msgseq const msgSeq: string = emojiLikeData.gtip.url.msgseq
const emojiId: string = emojiLikeData.gtip.face.id const emojiId: string = emojiLikeData.gtip.face.id
const peer = { const peer = {
chatType: ChatType.group, chatType: ChatType.Group,
guildId: '', guildId: '',
peerUid: msg.peerUid, peerUid: msg.peerUid,
} }
@@ -521,11 +518,11 @@ export namespace OB11Entities {
if (!replyMsgList?.length) { if (!replyMsgList?.length) {
return return
} }
const shortId = ctx.store.getShortIdByMsgInfo(peer, replyMsgList[0].msgId) const shortId = ctx.store.createMsgShortId(peer, replyMsgList[0].msgId)
return new OB11GroupMsgEmojiLikeEvent( return new OB11GroupMsgEmojiLikeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
parseInt(senderUin), parseInt(senderUin),
shortId!, shortId,
[{ [{
emoji_id: emojiId, emoji_id: emojiId,
count: 1, count: 1,
@@ -537,7 +534,7 @@ export namespace OB11Entities {
} }
if ( if (
grayTipElement.subElementType == GrayTipElementSubType.XMLMSG && grayTipElement.subElementType == GrayTipElementSubType.XmlMsg &&
xmlElement?.templId == '10179' xmlElement?.templId == '10179'
) { ) {
ctx.logger.info('收到新人被邀请进群消息', grayTipElement) ctx.logger.info('收到新人被邀请进群消息', grayTipElement)
@@ -571,8 +568,7 @@ export namespace OB11Entities {
pokedetail pokedetail
) )
} }
} } else if (grayTipElement.jsonGrayTipElement?.busiId === '2401' && json.items[2]) {
if (grayTipElement.jsonGrayTipElement?.busiId === '2401' && json.items[2]) {
ctx.logger.info('收到群精华消息', json) ctx.logger.info('收到群精华消息', json)
const searchParams = new URL(json.items[2].jp).searchParams const searchParams = new URL(json.items[2].jp).searchParams
const msgSeq = searchParams.get('seq') const msgSeq = searchParams.get('seq')
@@ -581,7 +577,7 @@ export namespace OB11Entities {
if (!groupCode || !msgSeq || !msgRandom) return if (!groupCode || !msgSeq || !msgRandom) return
const peer = { const peer = {
guildId: '', guildId: '',
chatType: ChatType.group, chatType: ChatType.Group,
peerUid: groupCode peerUid: groupCode
} }
const essence = await ctx.ntGroupApi.queryCachedEssenceMsg(groupCode, msgSeq, msgRandom) const essence = await ctx.ntGroupApi.queryCachedEssenceMsg(groupCode, msgSeq, msgRandom)
@@ -594,8 +590,7 @@ export namespace OB11Entities {
parseInt(essence.items[0]?.msgSenderUin ?? sourceMsg.senderUin), parseInt(essence.items[0]?.msgSenderUin ?? sourceMsg.senderUin),
parseInt(essence.items[0]?.opUin ?? '0'), parseInt(essence.items[0]?.opUin ?? '0'),
) )
} } else if (grayTipElement.jsonGrayTipElement?.busiId === '2407') {
if (grayTipElement.jsonGrayTipElement?.busiId === '2407') {
const memberUin = json.items[1].param[0] const memberUin = json.items[1].param[0]
const title = json.items[3].txt const title = json.items[3].txt
ctx.logger.info('收到群成员新头衔消息', json) ctx.logger.info('收到群成员新头衔消息', json)
@@ -605,6 +600,11 @@ export namespace OB11Entities {
} }
}) })
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title) return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
} else if (grayTipElement.jsonGrayTipElement?.busiId === '19217') {
ctx.logger.info('收到新人被邀请进群消息', grayTipElement)
const userId = new URL(json.items[2].jp).searchParams.get('robot_uin')
const operatorId = new URL(json.items[0].jp).searchParams.get('uin')
return new OB11GroupIncreaseEvent(Number(msg.peerUid), Number(userId), Number(operatorId), 'invite')
} }
} }
} }
@@ -617,13 +617,13 @@ export namespace OB11Entities {
shortId: number shortId: number
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> { ): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
const msgElement = msg.elements.find( const msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.REVOKE, (element) => element.grayTipElement?.subElementType === GrayTipElementSubType.Revoke,
) )
if (!msgElement) { if (!msgElement) {
return return
} }
const revokeElement = msgElement.grayTipElement!.revokeElement const revokeElement = msgElement.grayTipElement!.revokeElement
if (msg.chatType === ChatType.group) { if (msg.chatType === ChatType.Group) {
const operator = await ctx.ntGroupApi.getGroupMember(msg.peerUid, revokeElement!.operatorUid) const operator = await ctx.ntGroupApi.getGroupMember(msg.peerUid, revokeElement!.operatorUid)
return new OB11GroupRecallNoticeEvent( return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid), parseInt(msg.peerUid),
@@ -651,23 +651,20 @@ export namespace OB11Entities {
return friends.map(friend) return friends.map(friend)
} }
export function friendsV2(friends: FriendV2[]): OB11User[] { export function friendV2(raw: SimpleInfo): OB11User {
const data: OB11User[] = [] return {
for (const friend of friends) { ...omit(raw.baseInfo, ['richBuffer', 'phoneNum']),
const sexValue = sex(friend.baseInfo.sex!) ...omit(raw.coreInfo, ['nick']),
data.push({ user_id: parseInt(raw.coreInfo.uin),
...omit(friend.baseInfo, ['richBuffer']), nickname: raw.coreInfo.nick,
...friend.coreInfo, remark: raw.coreInfo.remark || raw.coreInfo.nick,
user_id: parseInt(friend.coreInfo.uin), sex: sex(raw.baseInfo.sex),
nickname: friend.coreInfo.nick, level: 0
remark: friend.coreInfo.nick,
sex: sexValue,
level: 0,
categroyName: friend.categroyName,
categoryId: friend.categoryId
})
} }
return data }
export function friendsV2(raw: SimpleInfo[]): OB11User[] {
return raw.map(friendV2)
} }
export function groupMemberRole(role: number): OB11GroupMemberRole | undefined { export function groupMemberRole(role: number): OB11GroupMemberRole | undefined {

View File

@@ -8,17 +8,15 @@ export interface MsgEmojiLike {
export class OB11GroupMsgEmojiLikeEvent extends OB11GroupNoticeEvent { export class OB11GroupMsgEmojiLikeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_msg_emoji_like' notice_type = 'group_msg_emoji_like'
message_id: number message_id: number
sub_type?: 'ban' | 'lift_ban'
likes: MsgEmojiLike[] likes: MsgEmojiLike[]
group_id: number group_id: number
user_id: number user_id: number
constructor(groupId: number, userId: number, messageId: number, likes: MsgEmojiLike[], sub_type?: 'ban' | 'lift_ban') { constructor(groupId: number, userId: number, messageId: number, likes: MsgEmojiLike[]) {
super() super()
this.group_id = groupId this.group_id = groupId
this.user_id = userId // 可为空表示是对别人的消息操作如果是对bot自己的消息则不为空 this.user_id = userId // 可为空表示是对别人的消息操作如果是对bot自己的消息则不为空
this.message_id = messageId this.message_id = messageId
this.likes = likes this.likes = likes
this.sub_type = sub_type
} }
} }

View File

@@ -15,7 +15,7 @@ import {
} from '../types' } from '../types'
import { decodeCQCode } from '../cqcode' import { decodeCQCode } from '../cqcode'
import { Peer } from '@/ntqqapi/types/msg' import { Peer } from '@/ntqqapi/types/msg'
import { SendElementEntities } from '@/ntqqapi/entities' import { SendElement } from '@/ntqqapi/entities'
import { selfInfo } from '@/common/globalVars' import { selfInfo } from '@/common/globalVars'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { Context } from 'cordis' import { Context } from 'cordis'
@@ -36,7 +36,7 @@ export async function createSendElements(
case OB11MessageDataType.text: { case OB11MessageDataType.text: {
const text = sendMsg.data?.text const text = sendMsg.data?.text
if (text) { if (text) {
sendElements.push(SendElementEntities.text(sendMsg.data!.text)) sendElements.push(SendElement.text(sendMsg.data!.text))
} }
} }
break break
@@ -62,22 +62,22 @@ export async function createSendElements(
} }
} }
if (isAdmin && remainAtAllCount > 0) { if (isAdmin && remainAtAllCount > 0) {
sendElements.push(SendElementEntities.at(atQQ, atQQ, AtType.atAll, '@全体成员')) sendElements.push(SendElement.at(atQQ, atQQ, AtType.All, '@全体成员'))
} }
} }
else if (peer.chatType === ChatType.group) { else if (peer.chatType === ChatType.Group) {
const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ) const atMember = await ctx.ntGroupApi.getGroupMember(peer.peerUid, atQQ)
if (atMember) { if (atMember) {
const display = `@${atMember.cardName || atMember.nick}` const display = `@${atMember.cardName || atMember.nick}`
sendElements.push( sendElements.push(
SendElementEntities.at(atQQ, atMember.uid, AtType.atUser, display), SendElement.at(atQQ, atMember.uid, AtType.One, display),
) )
} else { } else {
const atNmae = sendMsg.data?.name const atNmae = sendMsg.data?.name
const uid = await ctx.ntUserApi.getUidByUin(atQQ) || '' const uid = await ctx.ntUserApi.getUidByUin(atQQ) || ''
const display = atNmae ? `@${atNmae}` : '' const display = atNmae ? `@${atNmae}` : ''
sendElements.push( sendElements.push(
SendElementEntities.at(atQQ, uid, AtType.atUser, display), SendElement.at(atQQ, uid, AtType.One, display),
) )
} }
} }
@@ -97,7 +97,7 @@ export async function createSendElements(
)).msgList[0] )).msgList[0]
if (replyMsg) { if (replyMsg) {
sendElements.push( sendElements.push(
SendElementEntities.reply( SendElement.reply(
replyMsg.msgSeq, replyMsg.msgSeq,
replyMsg.msgId, replyMsg.msgId,
replyMsg.senderUin!, replyMsg.senderUin!,
@@ -111,13 +111,13 @@ export async function createSendElements(
case OB11MessageDataType.face: { case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id const faceId = sendMsg.data?.id
if (faceId) { if (faceId) {
sendElements.push(SendElementEntities.face(parseInt(faceId))) sendElements.push(SendElement.face(parseInt(faceId)))
} }
} }
break break
case OB11MessageDataType.mface: { case OB11MessageDataType.mface: {
sendElements.push( sendElements.push(
SendElementEntities.mface( SendElement.mface(
+sendMsg.data.emoji_package_id, +sendMsg.data.emoji_package_id,
sendMsg.data.emoji_id, sendMsg.data.emoji_id,
sendMsg.data.key, sendMsg.data.key,
@@ -127,20 +127,20 @@ export async function createSendElements(
} }
break break
case OB11MessageDataType.image: { case OB11MessageDataType.image: {
const res = await SendElementEntities.pic( const res = await SendElement.pic(
ctx, ctx,
(await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path, (await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })).path,
sendMsg.data.summary || '', sendMsg.data.summary || '',
sendMsg.data.subType || 0, sendMsg.data.subType || 0,
sendMsg.data.type === 'flash' sendMsg.data.type === 'flash'
) )
deleteAfterSentFiles.push(res.picElement.sourcePath) deleteAfterSentFiles.push(res.picElement.sourcePath!)
sendElements.push(res) sendElements.push(res)
} }
break break
case OB11MessageDataType.file: { case OB11MessageDataType.file: {
const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) const { path, fileName } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendElementEntities.file(ctx, path, fileName)) sendElements.push(await SendElement.file(ctx, path, fileName))
} }
break break
case OB11MessageDataType.video: { case OB11MessageDataType.video: {
@@ -150,38 +150,38 @@ export async function createSendElements(
const uri2LocalRes = await uri2local(thumb) const uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) thumb = uri2LocalRes.path if (uri2LocalRes.success) thumb = uri2LocalRes.path
} }
const res = await SendElementEntities.video(ctx, path, fileName, thumb) const res = await SendElement.video(ctx, path, fileName, thumb)
deleteAfterSentFiles.push(res.videoElement.filePath) deleteAfterSentFiles.push(res.videoElement.filePath)
sendElements.push(res) sendElements.push(res)
} }
break break
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles }) const { path } = await handleOb11FileLikeMessage(ctx, sendMsg, { deleteAfterSentFiles })
sendElements.push(await SendElementEntities.ptt(ctx, path)) sendElements.push(await SendElement.ptt(ctx, path))
} }
break break
case OB11MessageDataType.json: { case OB11MessageDataType.json: {
sendElements.push(SendElementEntities.ark(sendMsg.data.data)) sendElements.push(SendElement.ark(sendMsg.data.data))
} }
break break
case OB11MessageDataType.dice: { case OB11MessageDataType.dice: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendElementEntities.dice(resultId)) sendElements.push(SendElement.dice(resultId))
} }
break break
case OB11MessageDataType.RPS: { case OB11MessageDataType.RPS: {
const resultId = sendMsg.data?.result const resultId = sendMsg.data?.result
sendElements.push(SendElementEntities.rps(resultId)) sendElements.push(SendElement.rps(resultId))
} }
break break
case OB11MessageDataType.contact: { case OB11MessageDataType.contact: {
const { type, id } = sendMsg.data const { type, id } = sendMsg.data
const data = type === 'qq' ? ctx.ntFriendApi.getBuddyRecommendContact(id) : ctx.ntGroupApi.getGroupRecommendContact(id) const data = type === 'qq' ? ctx.ntFriendApi.getBuddyRecommendContact(id) : ctx.ntGroupApi.getGroupRecommendContact(id)
sendElements.push(SendElementEntities.ark(await data)) sendElements.push(SendElement.ark(await data))
} }
break break
case OB11MessageDataType.shake: { case OB11MessageDataType.shake: {
sendElements.push(SendElementEntities.shake()) sendElements.push(SendElement.shake())
} }
break break
} }
@@ -248,24 +248,32 @@ export async function sendMsg(
sendElements: SendMessageElement[], sendElements: SendMessageElement[],
deleteAfterSentFiles: string[] deleteAfterSentFiles: string[]
) { ) {
if (peer.chatType === ChatType.Group) {
const info = await ctx.ntGroupApi.getGroupAllInfo(peer.peerUid)
.catch(() => undefined)
const shutUpMeTimestamp = info?.groupAll.shutUpMeTimestamp
if (shutUpMeTimestamp && shutUpMeTimestamp * 1000 > Date.now()) {
throw new Error('当前处于被禁言状态')
}
}
if (!sendElements.length) { if (!sendElements.length) {
throw '消息体无法解析,请检查是否发送了不支持的消息类型' throw new Error('消息体无法解析,请检查是否发送了不支持的消息类型')
} }
// 计算发送的文件大小 // 计算发送的文件大小
let totalSize = 0 let totalSize = 0
for (const fileElement of sendElements) { for (const fileElement of sendElements) {
try { try {
if (fileElement.elementType === ElementType.PTT) { if (fileElement.elementType === ElementType.Ptt) {
totalSize += fs.statSync(fileElement.pttElement.filePath).size totalSize += fs.statSync(fileElement.pttElement.filePath!).size
} }
if (fileElement.elementType === ElementType.FILE) { if (fileElement.elementType === ElementType.File) {
totalSize += fs.statSync(fileElement.fileElement.filePath).size totalSize += fs.statSync(fileElement.fileElement.filePath).size
} }
if (fileElement.elementType === ElementType.VIDEO) { if (fileElement.elementType === ElementType.Video) {
totalSize += fs.statSync(fileElement.videoElement.filePath).size totalSize += fs.statSync(fileElement.videoElement.filePath).size
} }
if (fileElement.elementType === ElementType.PIC) { if (fileElement.elementType === ElementType.Pic) {
totalSize += fs.statSync(fileElement.picElement.sourcePath).size totalSize += fs.statSync(fileElement.picElement.sourcePath!).size
} }
} catch (e) { } catch (e) {
ctx.logger.warn('文件大小计算失败', e, fileElement) ctx.logger.warn('文件大小计算失败', e, fileElement)
@@ -276,8 +284,7 @@ export async function sendMsg(
//log('设置消息超时时间', timeout) //log('设置消息超时时间', timeout)
const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, timeout) const returnMsg = await ctx.ntMsgApi.sendMsg(peer, sendElements, timeout)
if (returnMsg) { if (returnMsg) {
returnMsg.msgShortId = ctx.store.createMsgShortId(peer, returnMsg.msgId) ctx.logger.info('消息发送', peer)
ctx.logger.info('消息发送', returnMsg.msgShortId)
deleteAfterSentFiles.map(path => fsPromise.unlink(path)) deleteAfterSentFiles.map(path => fsPromise.unlink(path))
return returnMsg return returnMsg
} }
@@ -294,10 +301,10 @@ export enum CreatePeerMode {
Group = 2 Group = 2
} }
export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode: CreatePeerMode): Promise<Peer> { export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode = CreatePeerMode.Normal): Promise<Peer> {
if ((mode === CreatePeerMode.Group || mode === CreatePeerMode.Normal) && payload.group_id) { if ((mode === CreatePeerMode.Group || mode === CreatePeerMode.Normal) && payload.group_id) {
return { return {
chatType: ChatType.group, chatType: ChatType.Group,
peerUid: payload.group_id.toString(), peerUid: payload.group_id.toString(),
} }
} }
@@ -306,7 +313,7 @@ export async function createPeer(ctx: Context, payload: CreatePeerPayload, mode:
if (!uid) throw new Error('无法获取用户信息') if (!uid) throw new Error('无法获取用户信息')
const isBuddy = await ctx.ntFriendApi.isBuddy(uid) const isBuddy = await ctx.ntFriendApi.isBuddy(uid)
return { return {
chatType: isBuddy ? ChatType.friend : ChatType.temp, chatType: isBuddy ? ChatType.C2C : ChatType.TempC2CFromGroup,
peerUid: uid, peerUid: uid,
} }
} }

View File

@@ -98,8 +98,15 @@ export interface OB11Message {
temp_source?: 0 | 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 temp_source?: 0 | 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9
} }
export interface OB11ForwardMessage extends OB11Message { export interface OB11ForwardMessage {
content: OB11MessageData[] | string content: OB11MessageData[] | string
sender: {
nickname: string
user_id: number
}
time: number
message_format: string //扩展
message_type: string //扩展
} }
export interface OB11Return<DataType> { export interface OB11Return<DataType> {

View File

@@ -1 +1 @@
export const version = '3.33.4' export const version = '3.34.0'

View File

@@ -4,7 +4,6 @@
"module": "CommonJS", "module": "CommonJS",
"outDir": "./dist", "outDir": "./dist",
"strict": true, "strict": true,
"isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,