Compare commits

..

9 Commits

Author SHA1 Message Date
idranme
e1176e18cd Merge pull request #328 from LLOneBot/dev
3.28.3
2024-08-10 23:19:09 +08:00
idranme
107f02f21f chore: 3.28.3 2024-08-10 23:17:38 +08:00
idranme
51f8db3a83 opt 2024-08-10 22:31:14 +08:00
idranme
25691a4124 sync 2024-08-10 22:09:35 +08:00
idranme
40f03e6401 sync 2024-08-10 21:34:28 +08:00
idranme
9f89094978 sync 2024-08-10 20:36:15 +08:00
idranme
04f837145c sync 2024-08-10 18:14:33 +08:00
idranme
6126920830 sync 2024-08-10 17:17:19 +08:00
idranme
5c219aa003 opt 2024-08-09 22:32:54 +08:00
37 changed files with 863 additions and 552 deletions

View File

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

View File

@@ -30,9 +30,9 @@
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.25", "@types/fluent-ffmpeg": "^2.1.25",
"@types/node": "^20.11.24", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^29.0.1", "electron": "^29.1.4",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.0", "vite": "^5.4.0",

View File

@@ -1,18 +1,14 @@
import { import {
CategoryFriend,
type Friend, type Friend,
type FriendRequest,
type Group, type Group,
type GroupMember, type GroupMember,
type SelfInfo, type SelfInfo,
User,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { type FileCache, type LLOneBotError } from './types' import { type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group' import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log' import { log } from './utils/log'
import { isNumeric } from './utils/helper' import { isNumeric } from './utils/helper'
import { NTQQFriendApi } from '../ntqqapi/api' import { NTQQFriendApi } from '../ntqqapi/api'
import { WebApiGroupMember } from '@/ntqqapi/api/webapi'
export const selfInfo: SelfInfo = { export const selfInfo: SelfInfo = {
uid: '', uid: '',
@@ -20,13 +16,8 @@ export const selfInfo: SelfInfo = {
nick: '', nick: '',
online: true, online: true,
} }
export const WebGroupData = {
GroupData: new Map<string, Array<WebApiGroupMember>>(),
GroupTime: new Map<string, number>(),
}
export let groups: Group[] = [] export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
httpServerError: '', httpServerError: '',
@@ -107,18 +98,4 @@ export async function getGroupMember(groupQQ: string | number, memberUinOrUid: s
member = getMember() member = getMember()
} }
return member return member
} }
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) {
for (const uid in uidMaps) {
if (uidMaps[uid] === uin) {
return uid
}
}
}
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号
export let rawFriends: CategoryFriend[] = []

View File

@@ -96,6 +96,29 @@ export function cacheFunc(ttl: number, customKey: string = '') {
} }
} }
export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) { export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) { function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>() const cache = new Map<string, { expiry: number; value: any }>()

View File

@@ -1,7 +1,7 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import * as fs from 'node:fs' import fs from 'node:fs'
import { Config } from '../common/types' import { Config } from '../common/types'
import { import {
CHANNEL_CHECK_VERSION, CHANNEL_CHECK_VERSION,
@@ -15,11 +15,9 @@ import {
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer'
import { DATA_DIR } from '../common/utils' import { DATA_DIR } from '../common/utils'
import { import {
friendRequests,
getGroupMember, getGroupMember,
llonebotError, llonebotError,
selfInfo, selfInfo,
uidMaps,
} from '../common/data' } from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook' import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor' import { OB11Constructor } from '../onebot11/constructor'
@@ -28,24 +26,23 @@ import {
GroupNotifies, GroupNotifies,
GroupNotifyTypes, GroupNotifyTypes,
RawMessage, RawMessage,
BuddyReqType,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http' import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket' import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import * as path from 'node:path' import path from 'node:path'
import { dbUtil } from '../common/db' import { dbUtil } from '../common/db'
import { setConfig } from './setConfig' import { setConfig } from './setConfig'
import { NTQQUserApi } from '../ntqqapi/api/user' import { NTQQUserApi, NTQQGroupApi, sentMessages } from '../ntqqapi/api'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import '../ntqqapi/wrapper' import '../ntqqapi/wrapper'
import { sentMessages } from '@/ntqqapi/api'
import { NTEventDispatch } from '../common/utils/EventTask' import { NTEventDispatch } from '../common/utils/EventTask'
import { wrapperConstructor, getSession } from '../ntqqapi/wrapper' import { wrapperConstructor, getSession } from '../ntqqapi/wrapper'
@@ -284,6 +281,7 @@ function onLoad() {
} }
log('收到群通知', notify) log('收到群通知', notify)
await dbUtil.addGroupNotify(notify) await dbUtil.addGroupNotify(notify)
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
// let member2: GroupMember; // let member2: GroupMember;
// if (notify.user2.uid) { // if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid); // member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
@@ -342,48 +340,47 @@ function onLoad() {
} }
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) { else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求') log('有加群请求')
let requestQQ = uidMaps[notify.user1.uid] let requestQQ = ''
if (!requestQQ) { try {
try { // uid-->uin
requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid))
if (isNaN(parseInt(requestQQ))) {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) {
log('获取加群人QQ号失败', e)
} }
} catch (e) {
log('获取加群人QQ号失败 Uid:', notify.user1.uid, e)
} }
let invitorId: number let invitorId: string
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) { if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite' // groupRequestEvent.sub_type = 'invite'
let invitorQQ = uidMaps[notify.user2.uid] try {
if (!invitorQQ) { // uid-->uin
try { invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid))
let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)) if (isNaN(parseInt(invitorId))) {
invitorId = parseInt(invitor.uin) invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin
} catch (e) {
invitorId = 0
log('获取邀请人QQ号失败', e)
} }
} catch (e) {
invitorId = ''
log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e)
} }
} }
const groupRequestEvent = new OB11GroupRequestEvent( const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0, parseInt(requestQQ) || 0,
notify.seq, flag,
notify.postscript, notify.postscript,
invitorId!, invitorId! === undefined ? undefined : +invitorId,
'add' 'add'
) )
postOb11Event(groupRequestEvent) postOb11Event(groupRequestEvent)
} }
else if (notify.type == GroupNotifyTypes.INVITE_ME) { else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知') log('收到邀请我加群通知')
let userId = uidMaps[notify.user2.uid] const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || ''
if (!userId) {
userId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
const groupInviteEvent = new OB11GroupRequestEvent( const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode), parseInt(notify.group.groupCode),
parseInt(userId), parseInt(userId),
notify.seq, flag,
undefined, undefined,
undefined, undefined,
'invite' 'invite'
@@ -402,20 +399,24 @@ function onLoad() {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) { for (const req of payload.data.buddyReqs) {
const flag = req.friendUid + req.reqTime if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) { continue
friendRequests[flag] = req
log('有新的好友请求', req)
let userId: number
try {
const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
userId = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const friendRequestEvent = new OB11FriendRequestEvent(userId!, req.extWords, flag)
postOb11Event(friendRequestEvent)
} }
let userId = 0
try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin!)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
postOb11Event(friendRequestEvent)
} }
}) })
} }
@@ -431,11 +432,6 @@ function onLoad() {
} }
llonebotError.otherError = '' llonebotError.otherError = ''
startTime = Date.now() startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key
}
})
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! }) NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! })
log('start activate group member info') log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log) NTQQGroupApi.activateMemberInfoChange().then().catch(log)

View File

@@ -26,7 +26,7 @@ export class NTQQFileApi {
msgId, msgId,
elementId, elementId,
0, 0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url; { downSourceType: 1, triggerType: 1 }))?.urlResult?.domainUrl[0]?.url!
} }
static async getFileType(filePath: string) { static async getFileType(filePath: string) {

View File

@@ -1,7 +1,6 @@
import { Friend, FriendRequest, FriendV2 } from '../types' import { Friend, FriendV2 } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { friendRequests } from '@/common/data'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NTEventDispatch } from '@/common/utils/EventTask'
@@ -49,24 +48,18 @@ export class NTQQFriendApi {
} }
static async handleFriendRequest(flag: string, accept: boolean) { static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag] const data = flag.split('|')
if (!request) { if (data.length < 2) {
throw `flat: ${flag}, 对应的好友请求不存在` return
} }
const result = await callNTQQApi<GeneralCallResult>({ const friendUid = data[0]
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, const reqTime = data[1]
args: [ const session = getSession()
{ return session?.getBuddyService().approvalFriendRequest({
approvalInfo: { friendUid,
friendUid: request.friendUid, reqTime,
reqTime: request.reqTime, accept
accept,
},
},
],
}) })
delete friendRequests[flag]
return result
} }
static async getBuddyV2(refresh = false): Promise<FriendV2[]> { static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
@@ -103,6 +96,28 @@ export class NTQQFriendApi {
return retMap return retMap
} }
static async getBuddyV2ExWithCate(refresh = false) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const session = getSession()
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data : (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 NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
}
static async isBuddy(uid: string): Promise<boolean> { static async isBuddy(uid: string): Promise<boolean> {
const session = getSession() const session = getSession()
return session?.getBuddyService().isBuddy(uid)! return session?.getBuddyService().isBuddy(uid)!

View File

@@ -1,11 +1,10 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes } from '../types' import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } from '../types'
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { deleteGroup, uidMaps } from '../../common/data'
import { dbUtil } from '../../common/db'
import { log } from '../../common/utils/log'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindowApi, NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners'
export class NTQQGroupApi { export class NTQQGroupApi {
static async activateMemberListChange() { static async activateMemberListChange() {
@@ -37,22 +36,19 @@ export class NTQQGroupApi {
}) })
} }
static async getGroups(forced = false) { static async getGroups(forced = false): Promise<Group[]> {
// let cbCmd = ReceiveCmdS.GROUPS type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']
// if (process.platform != 'win32') { const [, , groupList] = await NTEventDispatch.CallNormalEvent
// cbCmd = ReceiveCmdS.GROUPS_STORE <(force: boolean) => Promise<any>, ListenerType>
// } (
const result = await callNTQQApi<{ 'NodeIKernelGroupService/getGroupList',
updateType: number 'NodeIKernelGroupListener/onGroupListUpdate',
groupList: Group[] 1,
}>({ 5000,
methodName: NTQQApiMethod.GROUPS, (updateType) => true,
args: [{ force_update: forced }, undefined], forced
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE], )
afterFirstCmd: false, return groupList
})
log('get groups result', result)
return result.groupList
} }
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
@@ -104,130 +100,64 @@ export class NTQQGroupApi {
) )
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify = await dbUtil.getGroupNotify(seq) const flagitem = flag.split('|')
if (!notify) { const groupCode = flagitem[0]
throw `${seq}对应的加群通知不存在` const seq = flagitem[1]
} const type = parseInt(flagitem[2])
// delete groupNotifies[seq] const session = getSession()
return await callNTQQApi<GeneralCallResult>({ return session?.getGroupService().operateSysNotify(
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, false,
args: [ {
{ 'operateType': operateType, // 2 拒绝
doubt: false, 'targetMsg': {
operateMsg: { 'seq': seq, // 通知序列号
operateType: operateType, // 2 拒绝 'type': type,
targetMsg: { 'groupCode': groupCode,
seq: seq, // 通知序列号 'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
type: notify.type, }
groupCode: notify.group.groupCode, })
postscript: reason,
},
},
},
null,
],
})
} }
static async quitGroup(groupQQ: string) { static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.QUIT_GROUP, return session?.getGroupService().quitGroup(groupQQ)
args: [{ groupCode: groupQQ }, null],
})
if (result.result === 0) {
deleteGroup(groupQQ)
}
return result
} }
static async kickMember( static async kickMember(
groupQQ: string, groupQQ: string,
kickUids: string[], kickUids: string[],
refuseForever: boolean = false, refuseForever = false,
kickReason: string = '', kickReason = '',
) { ) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.KICK_MEMBER, return session?.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
},
],
})
} }
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 // timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.MUTE_MEMBER, return session?.getGroupService().setMemberShutUp(groupQQ, memList)
args: [
{
groupCode: groupQQ,
memList,
},
],
})
} }
static async banGroup(groupQQ: string, shutUp: boolean) { static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.MUTE_GROUP, return session?.getGroupService().setGroupShutUp(groupQQ, shutUp)
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
} }
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
NTQQGroupApi.activateMemberListChange().then().catch(log) const session = getSession()
const res = await callNTQQApi<GeneralCallResult>({ return session?.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res
} }
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_MEMBER_ROLE, return session?.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
} }
static async setGroupName(groupQQ: string, groupName: string) { static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_GROUP_NAME, return session?.getGroupService().modifyGroupName(groupQQ, groupName, false)
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
} }
static async getGroupAtAllRemainCount(groupCode: string) { static async getGroupAtAllRemainCount(groupCode: string) {
@@ -252,19 +182,13 @@ export class NTQQGroupApi {
}) })
} }
static async getGroupRemainAtTimes(GroupCode: string) {
const session = getSession()
return session?.getGroupService().getGroupRemainAtTimes(GroupCode)!
}
// 头衔不可用 // 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) { static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title,
},
null,
],
})
} }
static publishGroupBulletin(groupQQ: string, title: string, content: string) { } static publishGroupBulletin(groupQQ: string, title: string, content: string) { }

View File

@@ -1,5 +1,5 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import { ChatType, RawMessage, SendMessageElement, Peer } from '../types' import { ChatType, RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { dbUtil } from '../../common/db' import { dbUtil } from '../../common/db'
import { selfInfo } from '../../common/data' import { selfInfo } from '../../common/data'
import { ReceiveCmdS, registerReceiveHook } from '../hook' import { ReceiveCmdS, registerReceiveHook } from '../hook'
@@ -67,6 +67,11 @@ async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 100
} }
export class NTQQMsgApi { export class NTQQMsgApi {
static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
const session = getSession()
return session?.getMsgService().getTempChatInfo(chatType, peerUid)!
}
static enterOrExitAIO(peer: Peer, enter: boolean) { static enterOrExitAIO(peer: Peer, enter: boolean) {
return callNTQQApi<GeneralCallResult>({ return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO, methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO,
@@ -85,6 +90,7 @@ export class NTQQMsgApi {
], ],
}) })
} }
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid

View File

@@ -1,8 +1,8 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types' import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { selfInfo, uidMaps, friends, groupMembers } from '@/common/data' import { selfInfo, friends, groupMembers } from '@/common/data'
import { cacheFunc, isQQ998, log, sleep, getBuildVersion } from '@/common/utils' import { CacheClassFuncAsync, isQQ998, log, sleep, getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services' import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
@@ -12,13 +12,6 @@ import { NTQQFriendApi } from './friend'
const userInfoCache: Record<string, User> = {} // uid: User const userInfoCache: Record<string, User> = {} // uid: User
export interface ClientKeyData extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}
export class NTQQUserApi { export class NTQQUserApi {
static async setQQAvatar(filePath: string) { static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
@@ -115,9 +108,6 @@ export class NTQQUserApi {
], ],
}) })
const info = result.info const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info return info
} }
// 首次请求两次才能拿到的等级信息 // 首次请求两次才能拿到的等级信息
@@ -165,21 +155,12 @@ export class NTQQUserApi {
return (await RequestUtil.HttpsGetCookies(url))?.skey return (await RequestUtil.HttpsGetCookies(url))?.skey
} }
@cacheFunc(60 * 30 * 1000) @CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) { static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) { const ClientKeyData = await NTQQUserApi.forceFetchClientKey()
let data = (await NTQQUserApi.getQzoneCookies()) const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue } return cookies
}
const skey = await this.getSkey()
const pskey = (await this.getPSkey([domain])).get(domain)
if (!pskey || !skey) {
throw new Error('获取Cookies失败')
}
const bkn = NTQQUserApi.genBkn(skey)
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`
return { cookies, bkn }
} }
static genBkn(sKey: string) { static genBkn(sKey: string) {
@@ -197,15 +178,15 @@ export class NTQQUserApi {
static async getPSkey(domains: string[]): Promise<Map<string, string>> { static async getPSkey(domains: string[]): Promise<Map<string, string>> {
const session = getSession() const session = getSession()
const res = await session?.getTipOffService().getPskey(domains, true) const res = await session?.getTipOffService().getPskey(domains, true)
if (res.result !== 0) { if (res?.result !== 0) {
throw new Error(`获取Pskey失败: ${res.errMsg}`) throw new Error(`获取Pskey失败: ${res?.errMsg}`)
} }
return res.domainPskeyMap return res.domainPskeyMap
} }
static async getClientKey(): Promise<ClientKeyData> { static async getClientKey() {
const session = getSession() const session = getSession()
return await session?.getTicketService().forceFetchClientKey('') return await session?.getTicketService().forceFetchClientKey('')!
} }
static async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> { static async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> {
@@ -291,4 +272,55 @@ export class NTQQUserApi {
Uin Uin
) )
} }
static async getUinByUidV1(Uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin',
5000,
[Uid]
)
let uin = ret.uinInfo.get(Uid)
if (!uin) {
//从Buddy缓存获取Uin
friends.forEach((t) => {
if (t.uid == Uid) {
uin = t.uin
}
})
}
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
}
return uin
}
static async getUinByUidV2(Uid: string) {
const session = getSession()
let uin = (await session?.getProfileService().getUinByUid('FriendsServiceImpl', [Uid]))?.get(Uid)
if (uin) return uin
uin = (await session?.getGroupService().getUinByUids([Uid]))?.uins.get(Uid)
if (uin) return uin
uin = (await session?.getUixConvertService().getUin([Uid]))?.uinInfo.get(Uid)
if (uin) return uin
uin = (await NTQQFriendApi.getBuddyIdMapCache(true)).getKey(Uid) //从Buddy缓存获取Uin
if (uin) return uin
uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(Uid)
if (uin) return uin
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
return uin
}
static async getUinByUid(Uid: string) {
if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUinByUidV2(Uid)
}
return await NTQQUserApi.getUinByUidV1(Uid)
}
@CacheClassFuncAsync(3600 * 1000, 'ClientKey')
static async forceFetchClientKey() {
const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')!
}
} }

View File

@@ -1,7 +1,8 @@
import { WebGroupData, groups, selfInfo } from '@/common/data' import { selfInfo } from '@/common/data'
import { log } from '@/common/utils/log' import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user' import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { CacheClassFuncAsync } from '@/common/utils/helper'
export enum WebHonorType { export enum WebHonorType {
ALL = 'all', ALL = 'all',
@@ -137,56 +138,44 @@ export class WebApi {
return ret return ret
} }
@CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members')
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode); //logDebug('webapi 获取群成员', GroupCode)
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>(); let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
try { try {
let cachedData = WebGroupData.GroupData.get(GroupCode); const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com')
let cachedTime = WebGroupData.GroupTime.get(GroupCode); const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const Bkn = WebApi.genBkn(CookiesObject.skey)
if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) { const retList: Promise<WebApiGroupMemberRet>[] = []
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
const _Skey = await NTQQUserApi.getSkey(); if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; return []
if (!_Skey || !_Pskey) {
return MemberData;
}
const Bkn = WebApi.genBkn(_Skey);
const retList: Promise<WebApiGroupMemberRet>[] = [];
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
} else {
for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key]);
}
}
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40);
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
retList.push(ret);
}
//批量等待
for (let i = 1; i <= PageNum; i++) {
const ret = await (retList[i]);
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue;
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key]);
}
}
WebGroupData.GroupData.set(GroupCode, MemberData);
WebGroupData.GroupTime.set(GroupCode, Date.now());
} else { } else {
MemberData = cachedData as Array<WebApiGroupMember>; for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key])
}
}
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
retList.push(ret)
}
//批量等待
for (let i = 1; i <= PageNum; i++) {
const ret = await (retList[i])
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key])
}
} }
} catch { } catch {
return MemberData; return MemberData
} }
return MemberData; return MemberData
} }
// public static async addGroupDigest(groupCode: string, msgSeq: string) { // public static async addGroupDigest(groupCode: string, msgSeq: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`; // const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`;

View File

@@ -77,7 +77,7 @@ export class SendMsgElementConstructor {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
const maxMB = 30; const maxMB = 30;
if (fileSize > 1024 * 1024 * 30){ if (fileSize > 1024 * 1024 * 30) {
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} }
const imageSize = await NTQQFileApi.getImageSize(picPath) const imageSize = await NTQQFileApi.getImageSize(picPath)
@@ -104,21 +104,21 @@ export class SendMsgElementConstructor {
} }
} }
static async file(filePath: string, fileName: string = ''): Promise<SendFileElement> { static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常,大小为 0'
} }
let element: SendFileElement = { const element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: '', elementId: '',
fileElement: { fileElement: {
fileName: fileName || _fileName, fileName: fileName || _fileName,
filePath: path, folderId: folderId,
filePath: path!,
fileSize: fileSize.toString(), fileSize: fileSize.toString(),
}, },
} }
return element return element
} }
@@ -175,7 +175,7 @@ export class SendMsgElementConstructor {
setTimeout(useDefaultThumb, 5000) setTimeout(useDefaultThumb, 5000)
ffmpeg(filePath) ffmpeg(filePath)
.on('end', () => {}) .on('end', () => { })
.on('error', (err) => { .on('error', (err) => {
if (diyThumbPath) { if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath) fs.copyFile(diyThumbPath, thumbPath)
@@ -280,10 +280,10 @@ export class SendMsgElementConstructor {
faceId = parseInt(faceId.toString()) faceId = parseInt(faceId.toString())
// let faceType = parseInt(faceId.toString().substring(0, 1)); // let faceType = parseInt(faceId.toString().substring(0, 1));
let faceType = 1 let faceType = 1
if (faceId >= 222){ if (faceId >= 222) {
faceType = 2 faceType = 2
} }
if (face?.AniStickerType){ if (face?.AniStickerType) {
faceType = 3; faceType = 3;
} }
return { return {

View File

@@ -1,16 +1,14 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import { NTQQMsgApi, sendMessagePool } from './api/msg' import { NTQQMsgApi, sendMessagePool } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types' import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types'
import { import {
deleteGroup, deleteGroup,
friends, friends,
getFriend, getFriend,
getGroupMember, getGroupMember,
groups, rawFriends, groups,
selfInfo, selfInfo,
tempGroupCodeMap,
uidMaps,
} from '@/common/data' } from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event' import { postOb11Event } from '../onebot11/server/post-ob11-event'
@@ -402,8 +400,6 @@ export async function startHook() {
registerReceiveHook<{ registerReceiveHook<{
data: CategoryFriend[] data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => { }>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0;
rawFriends.push(...payload.data);
for (const fData of payload.data) { for (const fData of payload.data) {
const _friends = fData.buddyList const _friends = fData.buddyList
for (let friend of _friends) { for (let friend of _friends) {
@@ -420,23 +416,6 @@ export async function startHook() {
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid
const uin = message.senderUin
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin
}
}
// 自动清理新消息文件 // 自动清理新消息文件
const { autoDeleteFile } = getConfigUtil().getConfig() const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) { if (!autoDeleteFile) {
@@ -459,10 +438,6 @@ export async function startHook() {
if (msgElement.picElement) { if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath)) pathList.push(...Object.values(msgElement.picElement.thumbPath))
} }
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
}
// log("需要清理的文件", pathList); // log("需要清理的文件", pathList);
for (const path of pathList) { for (const path of pathList) {

View File

@@ -1,19 +0,0 @@
import * as os from "os";
import path from "node:path";
import fs from "fs";
export function getModuleWithArchName(moduleName: string) {
const systemPlatform = os.platform()
const cpuArch = os.arch()
return `${moduleName}-${systemPlatform}-${cpuArch}.node`
}
export function cpModule(moduleName: string) {
const currentDir = path.resolve(__dirname);
const fileName = `./${getModuleWithArchName(moduleName)}`
try {
fs.copyFileSync(path.join(currentDir, fileName), path.join(currentDir, `${moduleName}.node`));
} catch (e) {
}
}

View File

@@ -1,58 +0,0 @@
import { log } from '../../../common/utils'
import { NTQQApi } from '../../ntcall'
import { cpModule } from '../cpmodule'
type PokeHandler = (id: string, isGroup: boolean) => void
type CrychicHandler = (event: string, id: string, isGroup: boolean) => void
let pokeRecords: Record<string, number> = {}
class Crychic {
private crychic: any = undefined
loadNode() {
if (!this.crychic) {
try {
cpModule('crychic')
this.crychic = require('./crychic.node')
this.crychic.init()
} catch (e) {
log('crychic加载失败', e)
}
}
}
registerPokeHandler(fn: PokeHandler) {
this.registerHandler((event, id, isGroup) => {
if (event === 'poke') {
let existTime = pokeRecords[id]
if (existTime) {
if (Date.now() - existTime < 1500) {
return
}
}
pokeRecords[id] = Date.now()
fn(id, isGroup)
}
})
}
registerHandler(fn: CrychicHandler) {
if (!this.crychic) return
this.crychic.setCryHandler(fn)
}
sendFriendPoke(friendUid: string) {
if (!this.crychic) return
this.crychic.sendFriendPoke(parseInt(friendUid))
NTQQApi.fetchUnitedCommendConfig().then()
}
sendGroupPoke(groupCode: string, memberUin: string) {
if (!this.crychic) return
this.crychic.sendGroupPoke(parseInt(memberUin), parseInt(groupCode))
NTQQApi.fetchUnitedCommendConfig().then()
}
}
export const crychic = new Crychic()

View File

@@ -1,33 +0,0 @@
import {cpModule} from "../cpmodule";
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface MoeHook {
GetRkey: () => string, // Return '&rkey=xxx'
HookRkey: (version: string) => string
}
class HookApi {
private readonly moeHook: MoeHook | null = null;
constructor() {
cpModule('MoeHoo');
try {
this.moeHook = require('./MoeHoo.node');
console.log("hook rkey qq version", this.moeHook!.HookRkey(qqPkgInfo.version));
console.log("hook rkey地址", this.moeHook!.HookRkey(qqPkgInfo.version));
} catch (e) {
console.log('加载 moehoo 失败', e);
}
}
getRKey(): string {
return this.moeHook?.GetRkey() || '';
}
isAvailable() {
return !!this.moeHook;
}
}
// export const hookApi = new HookApi();

View File

@@ -0,0 +1,270 @@
import { GetFileListParam, MessageElement, Peer } from '../types'
import { GeneralCallResult } from './common'
export enum UrlFileDownloadType {
KUNKNOWN,
KURLFILEDOWNLOADPRIVILEGEICON,
KURLFILEDOWNLOADPHOTOWALL,
KURLFILEDOWNLOADQZONE,
KURLFILEDOWNLOADCOMMON,
KURLFILEDOWNLOADINSTALLAPP
}
export enum RMBizTypeEnum {
KUNKNOWN,
KC2CFILE,
KGROUPFILE,
KC2CPIC,
KGROUPPIC,
KDISCPIC,
KC2CVIDEO,
KGROUPVIDEO,
KC2CPTT,
KGROUPPTT,
KFEEDCOMMENTPIC,
KGUILDFILE,
KGUILDPIC,
KGUILDPTT,
KGUILDVIDEO
}
export interface CommonFileInfo {
bizType: number
chatType: number
elemId: string
favId: string
fileModelId: string
fileName: string
fileSize: string
md5: string
md510m: string
msgId: string
msgTime: string
parent: string
peerUid: string
picThumbPath: Array<string>
sha: string
sha3: string
subId: string
uuid: string
}
export interface NodeIKernelRichMediaService {
//getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb)
// public enum VideoCodecFormatType {
// KCODECFORMATH264,
// KCODECFORMATH265,
// KCODECFORMATH266,
// KCODECFORMATAV1
// }
// public enum VideoRequestWay {
// KUNKNOW,
// KHAND,
// KAUTO
// }
getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise<unknown>
//exParams (RMReqExParams)
// this.downSourceType = i2
// this.triggerType = i3
//peer, msgId, elemId, videoCodecFormat, exParams
// 1 0 频道在用
// 1 1
// 0 2
// public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007
// public static final int KDOWNSOURCETYPEAIOINNER = 1
// public static final int KDOWNSOURCETYPEBIGSCREEN = 2
// public static final int KDOWNSOURCETYPEHISTORY = 3
// public static final int KDOWNSOURCETYPEUNKNOWN = 0
// public static final int KTRIGGERTYPEAUTO = 1
// public static final int KTRIGGERTYPEMANUAL = 0
getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & {
urlResult: {
v4IpUrl: [],
v6IpUrl: [],
domainUrl: Array<{
url: string,
isHttps: boolean,
httpsDomain: string
}>,
videoCodecFormat: number
}
}>
getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown
// this.senderUid = ""
// this.peerUid = ""
// this.guildId = ""
// this.elem = new MsgElement()
// this.downloadType = i2
// this.thumbSize = i3
// this.msgId = j2
// this.msgRandom = j3
// this.msgSeq = j4
// this.msgTime = j5
// this.chatType = i4
// this.senderUid = str
// this.peerUid = str2
// this.guildId = str3
// this.elem = msgElement
// this.useHttps = num
getVideoPlayUrlInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): Promise<unknown>
//arg双端number
isFileExpired(arg: number): unknown
deleteGroupFolder(GroupCode: string, FolderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>
//参数与getVideoPlayUrlInVisit一样
downloadRichMediaInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): unknown
//arg3为“”
downloadFileForModelId(peer: Peer, ModelId: string[], arg3: string): unknown
//第三个参数 Array<Type>
// this.fileId = ""
// this.fileName = ""
// this.fileId = str
// this.fileName = str2
// this.fileSize = j2
// this.fileModelId = j3
downloadFileForFileUuid(peer: Peer, uuid: string, arg3: {
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}[]): Promise<unknown>
downloadFileByUrlList(fileDownloadTyp: UrlFileDownloadType, urlList: Array<string>): unknown
downloadFileForFileInfo(fileInfo: CommonFileInfo[], savePath: string): unknown
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }>
downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown
createGroupFolder(arg1: unknown, arg2: unknown): unknown
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
deleteGroupFolder(arg1: unknown, arg2: unknown): unknown
deleteTransferInfo(arg1: unknown, arg2: unknown): unknown
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown
cancelUrlDownload(arg: unknown): unknown
updateOnlineVideoElemStatus(arg: unknown): unknown
getGroupSpace(arg: unknown): unknown
getGroupFileList(groupCode: string, params: GetFileListParam): Promise<GeneralCallResult & {
groupSpaceResult: {
retCode: number
retMsg: string
clientWording: string
totalSpace: number
usedSpace: number
allUpload: boolean
}
}>
getGroupFileInfo(arg1: unknown, arg2: unknown): unknown
getGroupTransferList(arg1: unknown, arg2: unknown): unknown
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
transGroupFile(arg1: unknown, arg2: unknown): unknown
searchGroupFile(
keywords: Array<string>,
param: {
groupIds: Array<string>,
fileType: number,
context: string,
count: number,
sortType: number,
groupNames: Array<string>
}): Promise<unknown>
searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & {
transGroupFileResult: {
result: any
successFileIdList: Array<any>
failFileIdList: Array<any>
}
}>
translateEnWordToZn(words: string[]): Promise<GeneralCallResult & { words: string[] }>
getScreenOCR(path: string): Promise<unknown>
batchGetGroupFileCount(Gids: Array<string>): Promise<GeneralCallResult & { groupCodes: Array<string>, groupFileCounts: Array<number> }>
queryPicDownloadSize(arg: unknown): unknown
searchGroupFile(arg1: unknown, arg2: unknown): unknown
searchMoreGroupFile(arg: unknown): unknown
cancelSearcheGroupFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown
onlyDownloadFile(peer: Peer, arg2: unknown, arg3: Array<{
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}
>): unknown
onlyUploadFile(arg1: unknown, arg2: unknown): unknown
isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown
uploadRMFileWithoutMsg(arg: {
bizType: RMBizTypeEnum,
filePath: string,
peerUid: string,
transferId: string
useNTV2: string
}): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,11 @@
import { forceFetchClientKeyRetType } from './common'
export interface NodeIKernelTicketService {
addKernelTicketListener(listener: unknown): void
removeKernelTicketListener(listenerId: unknown): void
forceFetchClientKey(arg: string): Promise<forceFetchClientKeyRetType>
isNull(): boolean
}

View File

@@ -0,0 +1,19 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelTipOffService {
addKernelTipOffListener(listener: unknown): void
removeKernelTipOffListener(listenerId: unknown): void
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
getPskey(domainList: string[], nocache: boolean): Promise<GeneralCallResult & { domainPskeyMap: Map<string, string> }> //2
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
tipOffMsgs(args: unknown[]): Promise<unknown> //1
encodeUinAesInfo(args: unknown[]): Promise<unknown> //2
isNull(): boolean
}

View File

@@ -4,4 +4,7 @@ export * from './NodeIKernelGroupService'
export * from './NodeIKernelProfileLikeService' export * from './NodeIKernelProfileLikeService'
export * from './NodeIKernelMsgService' export * from './NodeIKernelMsgService'
export * from './NodeIKernelMSFService' export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService' export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService'

View File

@@ -1,6 +1,15 @@
import { GroupMemberRole } from './group' import { GroupMemberRole } from './group'
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
}
export enum ElementType { export enum ElementType {
UNKNOWN = 0,
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
@@ -8,8 +17,30 @@ export enum ElementType {
VIDEO = 5, VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
WALLET = 9,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10, ARK = 10,
MFACE = 11, 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
} }
export interface SendTextElement { export interface SendTextElement {
@@ -101,18 +132,19 @@ export interface ReplyElement {
} }
export interface FileElement { export interface FileElement {
fileMd5?: '' fileMd5?: string
fileName: string fileName: string
filePath: string filePath: string
fileSize: string fileSize: string
picHeight?: number picHeight?: number
picWidth?: number picWidth?: number
picThumbPath?: {} folderId?: string
file10MMd5?: '' picThumbPath?: Map<number, string>
fileSha?: '' file10MMd5?: string
fileSha3?: '' fileSha?: string
fileUuid?: '' fileSha3?: string
fileSubId?: '' fileUuid?: string
fileSubId?: string
thumbFileSize?: number thumbFileSize?: number
fileBizId?: number fileBizId?: number
} }
@@ -158,6 +190,50 @@ export enum ChatType {
temp = 100, temp = 100,
} }
// 来自Android分析
export enum ChatType2 {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
}
export interface PttElement { export interface PttElement {
canConvert2Text: boolean canConvert2Text: boolean
duration: number // 秒数 duration: number // 秒数
@@ -391,10 +467,12 @@ export interface RawMessage {
chatType: ChatType chatType: ChatType
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[]
elements: { elements: {
elementId: string elementId: string
elementType: ElementType elementType: ElementType
replyElement: { replyElement: {
sourceMsgIdInRecords: string
senderUid: string // 原消息发送者QQ号 senderUid: string // 原消息发送者QQ号
sourceMsgIsIncPic: boolean // 原消息是否有图片 sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string sourceMsgText: string

View File

@@ -48,8 +48,28 @@ export enum GroupRequestOperateTypes {
reject = 2, reject = 2,
} }
export enum BuddyReqType {
KMEINITIATOR,
KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
}
export interface FriendRequest { export interface FriendRequest {
isInitiator?: boolean
isDecide: boolean
friendUid: string friendUid: string
reqType: BuddyReqType
reqTime: string // 时间戳,秒 reqTime: string // 时间戳,秒
extWords: string // 申请人填写的验证消息 extWords: string // 申请人填写的验证消息
isUnread: boolean isUnread: boolean

View File

@@ -5,7 +5,10 @@ import {
NodeIKernelProfileLikeService, NodeIKernelProfileLikeService,
NodeIKernelMSFService, NodeIKernelMSFService,
NodeIKernelMsgService, NodeIKernelMsgService,
NodeIKernelUixConvertService NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService
} from './services' } from './services'
import os from 'node:os' import os from 'node:os'
const Process = require('node:process') const Process = require('node:process')
@@ -19,6 +22,9 @@ export interface NodeIQQNTWrapperSession {
getMsgService(): NodeIKernelMsgService getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService getUixConvertService(): NodeIKernelUixConvertService
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
} }
export interface WrapperApi { export interface WrapperApi {

View File

@@ -1,19 +1,59 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { getUidByUin, uidMaps } from '../../../common/data'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel'
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { interface Payload {
user_id: number | string
}
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> { protected async _handle(payload: Payload): Promise<OB11User> {
const user_id = payload.user_id.toString() if (!(getBuildVersion() >= 26702)) {
const uid = getUidByUin(user_id) const user_id = payload.user_id.toString()
if (!uid) { const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id)
throw new Error('查无此人') const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick,
sex: OB11UserSex.unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} else {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} }
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
} }
} }

View File

@@ -1,50 +1,72 @@
import fs from 'node:fs'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { getGroup, getUidByUin } from '@/common/data' import { getGroup } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor' import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types' import { ChatType, SendFileElement } from '@/ntqqapi/types'
import fs from 'fs'
import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import { uri2local } from '@/common/utils' import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../msg/SendMsg'
import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
user_id: number user_id: number | string
group_id?: number group_id?: number | string
file: string file: string
name: string name: string
folder: string folder?: string
folder_id?: string
} }
class GoCQHTTPUploadFileBase extends BaseAction<Payload, null> { export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile actionName = ActionName.GoCQHTTP_UploadGroupFile
getPeer(payload: Payload): Peer {
if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! }
}
return { chatType: ChatType.group, peerUid: payload.group_id?.toString()! }
}
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id?.toString()!)
if (!group) {
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file let file = payload.file
if (fs.existsSync(file)) { if (fs.existsSync(file)) {
file = `file://${file}` file = `file://${file}`
} }
const downloadResult = await uri2local(file) const downloadResult = await uri2local(file)
if (downloadResult.errMsg) { if (!downloadResult.success) {
throw new Error(downloadResult.errMsg) throw new Error(downloadResult.errMsg)
} }
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name) const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id)
await NTQQMsgApi.sendMsg(this.getPeer(payload), [sendFileEle]) await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true)
return null return null
} }
} }
export class GoCQHTTPUploadGroupFile extends GoCQHTTPUploadFileBase { export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
}
export class GoCQHTTPUploadPrivateFile extends GoCQHTTPUploadFileBase {
actionName = ActionName.GoCQHTTP_UploadPrivateFile actionName = ActionName.GoCQHTTP_UploadPrivateFile
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw `私聊${payload.user_id}不存在`
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid)
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid }
}
throw '缺少参数 user_id'
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload)
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
}
const downloadResult = await uri2local(file)
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name)
await sendMsg(peer, [sendFileEle], [], true)
return null
}
} }

View File

@@ -5,22 +5,19 @@ import { NTQQGroupApi } from '../../../ntqqapi/api/group'
interface Payload { interface Payload {
flag: string flag: string
// sub_type: "add" | "invite", approve?: boolean | string
// type: "add" | "invite" reason?: string
approve: boolean
reason: string
} }
export default class SetGroupAddRequest extends BaseAction<Payload, null> { export default class SetGroupAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupAddRequest actionName = ActionName.SetGroupAddRequest
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString() const flag = payload.flag.toString()
const approve = payload.approve.toString() === 'true' const approve = payload.approve?.toString() !== 'false'
await NTQQGroupApi.handleGroupRequest( await NTQQGroupApi.handleGroupRequest(flag,
seq,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason, payload.reason || ''
) )
return null return null
} }

View File

@@ -1,10 +1,8 @@
import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types' import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types'
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { uidMaps } from '../../../common/data'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { NTQQUserApi } from '../../../ntqqapi/api/user'
import { NTQQGroupApi } from '../../../ntqqapi/api/group' import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { log } from '../../../common/utils/log'
interface OB11GroupRequestNotify { interface OB11GroupRequestNotify {
group_id: number group_id: number
@@ -17,11 +15,10 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> { protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await NTQQGroupApi.getGroupIgnoreNotifies() const data = await NTQQGroupApi.getGroupIgnoreNotifies()
log(data) const notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE)
let notifies: GroupNotify[] = data.notifies.filter((notify) => notify.status === GroupNotifyStatus.WAIT_HANDLE) const returnData: OB11GroupRequestNotify[] = []
let returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) { for (const notify of notifies) {
const uin = uidMaps[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin const uin = await NTQQUserApi.getUinByUid(notify.user1.uid)
returnData.push({ returnData.push({
group_id: parseInt(notify.group.groupCode), group_id: parseInt(notify.group.groupCode),
user_id: parseInt(uin), user_id: parseInt(uin),

View File

@@ -1,15 +1,14 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQMsgApi } from '@/ntqqapi/api' import { NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType, RawMessage } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { dbUtil } from '@/common/db' import { dbUtil } from '@/common/db'
import { getUidByUin } from '@/common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
import { Peer } from '@/ntqqapi/types' import { Peer } from '@/ntqqapi/types'
interface Payload { interface Payload {
message_id: number message_id: number
group_id: number group_id: number | string
user_id?: number user_id?: number | string
} }
interface Response { interface Response {
@@ -19,13 +18,20 @@ interface Response {
abstract class ForwardSingleMsg extends BaseAction<Payload, Response> { abstract class ForwardSingleMsg extends BaseAction<Payload, Response> {
protected async getTargetPeer(payload: Payload): Promise<Peer> { protected async getTargetPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) { if (payload.user_id) {
return { chatType: ChatType.friend, peerUid: getUidByUin(payload.user_id.toString())! } const peerUid = await NTQQUserApi.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() } return { chatType: ChatType.group, peerUid: payload.group_id!.toString() }
} }
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const msg = (await dbUtil.getMsgByShortId(payload.message_id))! const msg = await dbUtil.getMsgByShortId(payload.message_id)
if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`)
}
const peer = await this.getTargetPeer(payload) const peer = await this.getTargetPeer(payload)
const sentMsg = await NTQQMsgApi.forwardMsg( const sentMsg = await NTQQMsgApi.forwardMsg(
{ {

View File

@@ -2,14 +2,12 @@ import {
AtType, AtType,
ChatType, ChatType,
ElementType, ElementType,
Friend,
Group,
GroupMemberRole, GroupMemberRole,
PicSubType, PicSubType,
RawMessage, RawMessage,
SendMessageElement, SendMessageElement,
} from '../../../ntqqapi/types' } from '../../../ntqqapi/types'
import { friends, getGroup, getGroupMember, getUidByUin, selfInfo } from '../../../common/data' import { getGroup, getGroupMember, selfInfo } from '../../../common/data'
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
@@ -27,7 +25,7 @@ import { ActionName, BaseCheckResult } from '../types'
import fs from 'node:fs' import fs from 'node:fs'
import { decodeCQCode } from '../../cqcode' import { decodeCQCode } from '../../cqcode'
import { dbUtil } from '../../../common/db' import { dbUtil } from '../../../common/db'
import { ALLOW_SEND_TEMP_MSG, getConfigUtil } from '../../../common/config' import { getConfigUtil } from '../../../common/config'
import { log } from '../../../common/utils/log' import { log } from '../../../common/utils/log'
import { sleep } from '../../../common/utils/helper' import { sleep } from '../../../common/utils/helper'
import { uri2local } from '../../../common/utils' import { uri2local } from '../../../common/utils'

View File

@@ -5,9 +5,8 @@ import { OB11Message, OB11MessageAt, OB11MessageData, OB11MessageDataType } from
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest' import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest'
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest' import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'
import { dbUtil } from '@/common/db' import { dbUtil } from '@/common/db'
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/ntqqapi/api' import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/ntqqapi/api'
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types' import { ChatType, GroupRequestOperateTypes, Peer } from '@/ntqqapi/types'
import { getGroup, getUidByUin } from '@/common/data'
import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg' import { convertMessage2List, createSendElements, sendMsg } from './msg/SendMsg'
import { isNull, log } from '@/common/utils' import { isNull, log } from '@/common/utils'
import { getConfigUtil } from '@/common/config' import { getConfigUtil } from '@/common/config'
@@ -62,16 +61,15 @@ export async function handleQuickOperation(context: QuickOperationEvent, quickAc
} }
async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) { async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMessage | QuickOperationGroupMessage) {
msg = msg as OB11Message
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id) const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
const reply = quickAction.reply const reply = quickAction.reply
const ob11Config = getConfigUtil().getConfig().ob11 const ob11Config = getConfigUtil().getConfig().ob11
let peer: Peer = { const peer: Peer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: msg.user_id.toString(), peerUid: msg.user_id.toString(),
} }
if (msg.message_type == 'private') { if (msg.message_type == 'private') {
peer.peerUid = getUidByUin(msg.user_id.toString())! peer.peerUid = (await NTQQUserApi.getUidByUin(msg.user_id.toString()))!
if (msg.sub_type === 'group') { if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp peer.chatType = ChatType.temp
} }
@@ -81,7 +79,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
peer.peerUid = msg.group_id?.toString()! peer.peerUid = msg.group_id?.toString()!
} }
if (reply) { if (reply) {
let group: Group | null = null
let replyMessage: OB11MessageData[] = [] let replyMessage: OB11MessageData[] = []
if (ob11Config.enableQOAutoQuote) { if (ob11Config.enableQOAutoQuote) {
replyMessage.push({ replyMessage.push({
@@ -93,7 +90,6 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
if (msg.message_type == 'group') { if (msg.message_type == 'group') {
group = (await getGroup(msg.group_id?.toString()!))!
if ((quickAction as QuickOperationGroupMessage).at_sender) { if ((quickAction as QuickOperationGroupMessage).at_sender) {
replyMessage.push({ replyMessage.push({
type: 'at', type: 'at',
@@ -104,8 +100,7 @@ async function handleMsg(msg: OB11Message, quickAction: QuickOperationPrivateMes
} }
} }
replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape)) replyMessage = replyMessage.concat(convertMessage2List(reply, quickAction.auto_escape))
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group!) const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, peer)
log(`发送消息给`, peer, sendElements)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log) sendMsg(peer, sendElements, deleteAfterSentFiles, false).then().catch(log)
} }
if (msg.message_type === 'group') { if (msg.message_type === 'group') {

View File

@@ -1,16 +1,24 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import { NTQQUserApi } from '@/ntqqapi/api' import { NTQQUserApi, WebApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
interface Response {
cookies: string
bkn: string
}
interface Payload { interface Payload {
domain: string domain: string
} }
export class GetCookies extends BaseAction<Payload, { cookies: string; bkn: string }> { export class GetCookies extends BaseAction<Payload, Response> {
actionName = ActionName.GetCookies actionName = ActionName.GetCookies
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const domain = payload.domain || 'qun.qq.com' const cookiesObject = await NTQQUserApi.getCookies(payload.domain)
return NTQQUserApi.getCookies(domain); //把获取到的cookiesObject转换成 k=v; 格式字符串拼接在一起
const cookies = Object.entries(cookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const bkn = WebApi.genBkn(cookiesObject.p_skey)
return { cookies, bkn }
} }
} }

View File

@@ -1,11 +1,10 @@
import BaseAction from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Constructor } from '../../constructor'
import { friends, rawFriends } from '@/common/data' import { friends } from '@/common/data'
import BaseAction from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQFriendApi } from '@/ntqqapi/api' import { NTQQFriendApi } from '@/ntqqapi/api'
import { CategoryFriend } from '@/ntqqapi/types' import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface Payload { interface Payload {
no_cache: boolean | string no_cache: boolean | string
@@ -15,7 +14,7 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
actionName = ActionName.GetFriendList actionName = ActionName.GetFriendList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if (+qqPkgInfo.buildVersion >= 26702) { if (getBuildVersion() >= 26702) {
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true')) return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2(payload?.no_cache === true || payload?.no_cache === 'true'))
} }
if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') { if (friends.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
@@ -30,11 +29,16 @@ export class GetFriendList extends BaseAction<Payload, OB11User[]> {
} }
} }
// extend
export class GetFriendWithCategory extends BaseAction<void, Array<CategoryFriend>> { export class GetFriendWithCategory extends BaseAction<void, any> {
actionName = ActionName.GetFriendsWithCategory; actionName = ActionName.GetFriendsWithCategory;
protected async _handle(payload: void) { protected async _handle(payload: void) {
return rawFriends; if (getBuildVersion() >= 26702) {
//全新逻辑
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2ExWithCate(true))
} else {
throw new Error('this ntqq version not support, must be 26702 or later')
}
} }
} }

View File

@@ -4,7 +4,7 @@ import { NTQQFriendApi } from '../../../ntqqapi/api/friend'
interface Payload { interface Payload {
flag: string flag: string
approve: boolean approve?: boolean | string
remark?: string remark?: string
} }
@@ -12,7 +12,7 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest actionName = ActionName.SetFriendAddRequest
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const approve = payload.approve.toString() === 'true' const approve = payload.approve?.toString() !== 'false'
await NTQQFriendApi.handleFriendRequest(payload.flag, approve) await NTQQFriendApi.handleFriendRequest(payload.flag, approve)
return null return null
} }

View File

@@ -24,9 +24,10 @@ import {
TipGroupElementType, TipGroupElementType,
User, User,
VideoElement, VideoElement,
FriendV2 FriendV2,
ChatType2
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getGroupMember, selfInfo, tempGroupCodeMap, uidMaps } from '../common/data' import { deleteGroup, getGroupMember, selfInfo } from '../common/data'
import { EventType } from './event/OB11BaseEvent' import { EventType } from './event/OB11BaseEvent'
import { encodeCQCode } from './cqcode' import { encodeCQCode } from './cqcode'
import { dbUtil } from '../common/db' import { dbUtil } from '../common/db'
@@ -59,7 +60,6 @@ export class OB11Constructor {
debug, debug,
ob11: { messagePostFormat }, ob11: { messagePostFormat },
} = config } = config
const message_type = msg.chatType == ChatType.group ? 'group' : 'private'
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin), self_id: parseInt(selfInfo.uin),
user_id: parseInt(msg.senderUin!), user_id: parseInt(msg.senderUin!),
@@ -67,7 +67,7 @@ export class OB11Constructor {
message_id: msg.msgShortId!, message_id: msg.msgShortId!,
real_id: msg.msgShortId!, real_id: msg.msgShortId!,
message_seq: msg.msgShortId!, message_seq: msg.msgShortId!,
message_type: msg.chatType == ChatType.group ? 'group' : 'private', message_type: msg.chatType === ChatType.group ? 'group' : 'private',
sender: { sender: {
user_id: parseInt(msg.senderUin!), user_id: parseInt(msg.senderUin!),
nickname: msg.sendNickName, nickname: msg.sendNickName,
@@ -96,11 +96,15 @@ export class OB11Constructor {
resMsg.sub_type = 'friend' resMsg.sub_type = 'friend'
resMsg.sender.nickname = (await NTQQUserApi.getUserDetailInfo(msg.senderUid)).nick resMsg.sender.nickname = (await NTQQUserApi.getUserDetailInfo(msg.senderUid)).nick
} }
else if (msg.chatType == ChatType.temp) { else if (msg.chatType as unknown as ChatType2 == ChatType2.KCHATTYPETEMPC2CFROMGROUP) {
resMsg.sub_type = 'group' resMsg.sub_type = 'group'
const tempGroupCode = tempGroupCodeMap[msg.peerUin] const ret = await NTQQMsgApi.getTempChatInfo(ChatType2.KCHATTYPETEMPC2CFROMGROUP, msg.senderUid)
if (tempGroupCode) { if (ret.result === 0) {
resMsg.group_id = parseInt(tempGroupCode) resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode)
resMsg.sender.nickname = ret.tmpChatInfo!.fromNick
} else {
resMsg.group_id = 284840486 //兜底数据
resMsg.sender.nickname = '临时会话'
} }
} }
@@ -328,7 +332,11 @@ export class OB11Constructor {
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) return new OB11FriendPokeEvent(
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
} }
} }
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE //下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
@@ -533,7 +541,12 @@ export class OB11Constructor {
//筛选item带有uid的元素 //筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid) const poke_uid = pokedetail.filter(item => item.uid)
if (poke_uid.length == 2) { if (poke_uid.length == 2) {
return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((uidMaps[poke_uid[0].uid])!), parseInt((uidMaps[poke_uid[1].uid])), pokedetail) return new OB11GroupPokeEvent(
parseInt(msg.peerUid),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[0].uid)),
parseInt(await NTQQUserApi.getUinByUid(poke_uid[1].uid)),
pokedetail
)
} }
} }
if (grayTipElement.jsonGrayTipElement.busiId == 2401) { if (grayTipElement.jsonGrayTipElement.busiId == 2401) {

View File

@@ -411,10 +411,7 @@ async function onSettingWindowCreated(view: Element) {
buttonDom.addEventListener('click', async () => { buttonDom.addEventListener('click', async () => {
window.llonebot.checkVersion().then(checkVersionFunc) window.llonebot.checkVersion().then(checkVersionFunc)
}) })
} else if (!ResultVersion.result) {
return
}
if (!ResultVersion.result) {
titleDom.innerHTML = '当前已是最新版本 v' + version titleDom.innerHTML = '当前已是最新版本 v' + version
buttonDom.innerHTML = '无需更新' buttonDom.innerHTML = '无需更新'
} else { } else {

View File

@@ -1 +1 @@
export const version = '3.28.2' export const version = '3.28.3'