Compare commits

...

22 Commits

Author SHA1 Message Date
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
51 changed files with 760 additions and 679 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.33.9",
"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",

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

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

@@ -30,7 +30,7 @@ export default class Store extends Service {
constructor(protected ctx: Context) { constructor(protected ctx: Context) {
super(ctx, 'store', true) super(ctx, 'store', true)
this.cache = new LimitedHashTable<string, number>(1000) this.cache = new LimitedHashTable(1000)
this.initDatabase() this.initDatabase()
} }

View File

@@ -23,7 +23,6 @@ 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 {
@@ -73,7 +72,7 @@ export class NTQQFileApi extends Service {
} }
// 上传文件到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 +106,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 +146,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) {
@@ -201,6 +194,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 +236,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,8 +1,7 @@
import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types' import { Friend, FriendV2, SimpleInfo, CategoryFriend, BuddyListReqType } 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 { Dict, pick } from 'cosmokit'
import { Service, Context } from 'cordis' import { Service, Context } from 'cordis'
@@ -102,52 +101,20 @@ 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> {

View File

@@ -6,8 +6,9 @@ import {
GroupNotifies, GroupNotifies,
GroupRequestOperateTypes, GroupRequestOperateTypes,
GetFileListParam, GetFileListParam,
OnGroupFileInfoUpdateParams, PublishGroupBulletinReq,
PublishGroupBulletinReq GroupAllInfo,
GroupFileInfo
} 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 +66,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 +105,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 +157,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 +273,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 +289,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 +327,24 @@ 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
}
)
}
} }

View File

@@ -267,6 +267,12 @@ export class NTQQMsgApi extends Service {
} }
async generateMsgUniqueId(chatType: number) { async generateMsgUniqueId(chatType: number) {
return await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType }]) const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType }])
if (typeof uniqueId === 'string') {
return uniqueId
} else {
const random = Math.trunc(Math.random() * 100)
return `${Date.now()}${random}`
}
} }
} }

View File

@@ -1,9 +1,8 @@
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg, UserDetailSource, ProfileBizType } 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 { 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'
@@ -157,30 +156,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)
} }
@@ -249,7 +242,7 @@ 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])
} }
} }

View File

@@ -63,7 +63,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,7 +116,7 @@ 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)
@@ -136,12 +136,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)
@@ -215,7 +215,7 @@ class Core extends Service {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs) this.ctx.parallel('nt/friend-request', payload.data.buddyReqs)
}) })
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,
@@ -27,11 +28,11 @@ import { isNullable } from 'cosmokit'
export namespace SendElementEntities { 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 +42,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 +56,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 +68,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 +81,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 +91,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 +104,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 +123,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 +133,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 +200,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 +212,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 +225,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 +233,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 +268,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 +284,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 +302,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 +324,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 +343,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 +355,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

@@ -98,7 +98,7 @@ interface NTService {
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 +115,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 +159,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,36 +1,10 @@
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>

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

@@ -78,3 +78,45 @@ 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
}

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

@@ -344,3 +344,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

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

@@ -36,8 +36,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, payload.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 +53,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 +67,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 +101,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 +158,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,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
} }
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.number().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

@@ -69,6 +69,7 @@ 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'
export function initActionMap(adapter: Adapter) { export function initActionMap(adapter: Adapter) {
const actionHandlers = [ const actionHandlers = [
@@ -143,6 +144,7 @@ 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)
] ]
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

@@ -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,7 +17,7 @@ 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.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`) throw new Error(`转发消息失败 ${ret.errMsg}`)

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

@@ -81,5 +81,6 @@ 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'
} }

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

@@ -185,11 +185,6 @@ class OneBot11Adapter extends Service {
if (parseInt(message.msgTime) < this.startTime / 1000) { if (parseInt(message.msgTime) < this.startTime / 1000) {
continue continue
} }
const peer: Peer = {
chatType: message.chatType,
peerUid: message.peerUid
}
message.msgShortId = this.ctx.store.createMsgShortId(peer, message.msgId)
this.addMsgCache(message) this.addMsgCache(message)
OB11Entities.message(this.ctx, message) OB11Entities.message(this.ctx, message)

View File

@@ -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() {
@@ -85,6 +78,8 @@ class OB11Http {
private authorize(req: Request, res: Response, next: () => void) { private authorize(req: Request, res: Response, next: () => void) {
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,31 @@ 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!' })) return res.status(403).json({ message: 'token verify failed!' })
} }
next() next()
} }
private registerRouter(method: 'post' | 'get', url: string, handler: RegisterHandler) { private async handleRequest(req: Request, res: Response, next: () => void) {
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

@@ -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()
} }
} }
} }
@@ -361,20 +359,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 +401,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 +421,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 +435,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 +465,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 +511,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,
} }
@@ -537,7 +535,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)
@@ -581,7 +579,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)
@@ -617,13 +615,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 +649,20 @@ export namespace OB11Entities {
return friends.map(friend) return friends.map(friend)
} }
export function friendsV2(friends: FriendV2[]): OB11User[] { export function friendV2(raw: FriendV2): 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: FriendV2[]): OB11User[] {
return raw.map(friendV2)
} }
export function groupMemberRole(role: number): OB11GroupMemberRole | undefined { export function groupMemberRole(role: number): OB11GroupMemberRole | undefined {

View File

@@ -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(SendElementEntities.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), SendElementEntities.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), SendElementEntities.at(atQQ, uid, AtType.One, display),
) )
} }
} }
@@ -134,7 +134,7 @@ export async function createSendElements(
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
@@ -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.33.9'