Compare commits

...

29 Commits

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

View File

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

View File

@@ -32,7 +32,7 @@
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.26",
"@types/node": "^20.14.15",
"@types/ws": "^8.5.12",
@@ -40,7 +40,7 @@
"electron-vite": "^2.3.0",
"protobufjs-cli": "^1.1.3",
"typescript": "^5.6.2",
"vite": "^5.4.7",
"vite": "^5.4.8",
"vite-plugin-cp": "^4.0.8"
},
"packageManager": "yarn@4.5.0"

View File

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

View File

@@ -38,3 +38,10 @@ export function mergeNewProperties(newObj: Dict, oldObj: Dict) {
export function filterNullable<T>(array: 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,42 +1,18 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { ElementType, MessageElement, Peer, RawMessage, QueryMsgsParams, TmpChatInfoApi } from '@/ntqqapi/types'
import { GeneralCallResult } from './common'
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 {
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>
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult & {
detailErr: Map<unknown, unknown>
}>
forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult>
@@ -97,7 +73,7 @@ export interface NodeIKernelMsgService {
downloadRichMedia(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): Promise<GeneralCallResult>
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -221,11 +221,6 @@ interface RelationFlags {
isHidePrivilegeIcon: number
}
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt {
constellation: number
shengXiao: number
@@ -255,7 +250,7 @@ interface PhotoWall {
picList: Pic[]
}
export interface UserDetailInfoListenerArg {
export interface UserDetailInfo {
uid: string
uin: string
simpleInfo: SimpleInfo
@@ -344,3 +339,21 @@ export interface UserDetailInfoByUin {
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,
guildId: ''
}
if (fileCache[0].elementType === ElementType.PIC) {
if (fileCache[0].elementType === ElementType.Pic) {
const msgList = await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (msgList.msgList.length === 0) {
throw new Error('msg not found')
@@ -59,7 +59,7 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
throw new Error('element not found')
}
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)
}
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) {
throw Error('找不到相关的聊天记录' + data?.errMsg)
}
const msgList = data.msgList
const messages = await Promise.all(
msgList.map(async (msg) => {
const resMsg = await OB11Entities.message(this.ctx, msg)
if (!resMsg) return
resMsg.message_id = this.ctx.store.createMsgShortId({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId)
return resMsg
const messages: (OB11ForwardMessage | undefined)[] = await Promise.all(
data.msgList.map(async (msg) => {
const res = await OB11Entities.message(this.ctx, msg)
if (res) {
return {
content: res.message,
sender: {
nickname: res.sender.nickname,
user_id: res.sender.user_id
},
time: res.time,
message_format: res.message_format,
message_type: res.message_type
}
}
})
)
const forwardMessages = filterNullable(messages)
.map(v => {
const msg = v as Partial<OB11ForwardMessage>
msg.content = msg.message
delete msg.message
return msg as OB11ForwardMessage
})
return { messages: forwardMessages }
return { messages: filterNullable(messages) }
}
}

View File

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

View File

@@ -4,7 +4,7 @@ import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types'
import { OB11Entities } from '../../entities'
import { RawMessage } from '@/ntqqapi/types'
import { filterNullable } from '@/common/utils/misc'
import { filterNullable, parseBool } from '@/common/utils/misc'
interface Payload {
group_id: number | string
@@ -23,12 +23,12 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
group_id: Schema.union([Number, String]).required(),
message_seq: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false),
reverseOrder: Schema.union([Boolean, Schema.transform(String, parseBool)]).default(false)
})
protected async _handle(payload: Payload): Promise<Response> {
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[]
if (!payload.message_seq || payload.message_seq === '0') {
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
@@ -39,9 +39,6 @@ export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
}
if (!msgList?.length) throw new Error('未找到消息')
if (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)))
return { messages: filterNullable(ob11MsgList) }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,26 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { GroupRequestOperateTypes } from '@/ntqqapi/types'
import { ActionName } from '../types'
import { parseBool } from '@/common/utils/misc'
interface Payload {
flag: string
approve?: boolean | string
approve: boolean
reason?: string
}
export default class SetGroupAddRequest extends BaseAction<Payload, null> {
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> {
const flag = payload.flag.toString()
const approve = payload.approve?.toString() !== 'false'
await this.ctx.ntGroupApi.handleGroupRequest(
flag,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.flag,
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason
)
return null

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,17 @@
import { BaseAction } from '../BaseAction'
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number
group_id: number | string
group_name: string
}
export default class SetGroupName extends BaseAction<Payload, null> {
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> {
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 { parseBool } from '@/common/utils/misc'
interface Payload {
group_id: number
group_id: number | string
enable: boolean
}
export default class SetGroupWholeBan extends BaseAction<Payload, null> {
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> {
const enable = payload.enable.toString() === 'true'
await this.ctx.ntGroupApi.banGroup(payload.group_id.toString(), enable)
await this.ctx.ntGroupApi.banGroup(payload.group_id.toString(), payload.enable)
return null
}
}

View File

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

View File

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

View File

@@ -4,14 +4,35 @@ import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
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
protected async _handle() {
if (getBuildVersion() >= 26702) {
return OB11Entities.friendsV2(await this.ctx.ntFriendApi.getBuddyV2ExWithCate(true))
} else {
if (getBuildVersion() < 26702) {
throw new Error('this ntqq version not support, must be 26702 or later')
}
const data = await this.ctx.ntFriendApi.getBuddyV2WithCate(true)
return data.buddyCategory.map(item => {
return {
categoryId: item.categoryId,
categorySortId: item.categorySortId,
categoryName: item.categroyName,
categoryMbCount: item.categroyMbCount,
onlineCount: item.onlineCount,
buddyList: item.buddyUids.map(uid => {
const info = data.userSimpleInfos[uid]
return OB11Entities.friendV2(info)
})
}
})
}
}

View File

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

View File

@@ -1,7 +1,6 @@
import { BaseAction } from '../BaseAction'
import { ChatType } from '@/ntqqapi/types'
import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types'
import { createPeer } from '@/onebot11/helper/createMessage'
interface Payload {
message_id: number | string
@@ -10,17 +9,6 @@ interface Payload {
}
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> {
if (!payload.message_id) {
throw Error('message_id不能为空')
@@ -29,10 +17,10 @@ abstract class ForwardSingleMsg extends BaseAction<Payload, null> {
if (!msg) {
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])
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`)
if (ret.length === 0) {
throw new Error(`转发消息失败`)
}
return null
}

View File

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

View File

@@ -93,7 +93,11 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnData> {
if (!returnMsg) {
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 {

View File

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

View File

@@ -3,6 +3,7 @@ import { OB11User } from '../../types'
import { OB11Entities } from '../../entities'
import { ActionName } from '../types'
import { getBuildVersion } from '@/common/utils'
import { parseBool } from '@/common/utils/misc'
interface Payload {
no_cache: boolean
@@ -11,7 +12,7 @@ interface Payload {
export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList
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) {

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import http from 'node:http'
import cors from 'cors'
import crypto from 'node:crypto'
import express, { Express, Request, Response } from 'express'
import express, { Express, Request, Response, NextFunction } from 'express'
import { BaseAction } from '../action/BaseAction'
import { Context } from 'cordis'
import { llonebotError, selfInfo } from '@/common/globalVars'
@@ -76,7 +76,7 @@ class OB11Http {
Object.assign(this.config, config)
}
private authorize(req: Request, res: Response, next: () => void) {
private authorize(req: Request, res: Response, next: NextFunction) {
const serverToken = this.config.token
if (!serverToken) return next()
@@ -95,12 +95,13 @@ class OB11Http {
}
if (clientToken !== serverToken) {
return res.status(403).json({ message: 'token verify failed!' })
res.status(403).json({ message: 'token verify failed!' })
} else {
next()
}
next()
}
private async handleRequest(req: Request, res: Response, next: () => void) {
private async handleRequest(req: Request, res: Response, next: NextFunction) {
if (req.path === '/') return next()
let payload = req.body
if (req.method === 'GET') {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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