Compare commits

..

30 Commits

Author SHA1 Message Date
linyuchen
bd212c4bf3 remove debug 2024-05-15 22:45:13 +08:00
linyuchen
32c7f904db fix: Http download headers 2024-05-15 22:44:15 +08:00
linyuchen
2ef017282f feat: get_group_honor_info 2024-05-15 22:33:55 +08:00
手瓜一十雪
9672f67a23 feat: new Api GetGroupEssence&GetGroupHonorInfo 2024-05-15 21:12:10 +08:00
手瓜一十雪
6e5cfd827c feat: webapi 2024-05-15 21:05:33 +08:00
手瓜一十雪
5402bef4a9 Merge branch 'main' into dev 2024-05-15 20:56:52 +08:00
linyuchen
4194512cce fix: Get cookies miss uin 2024-05-15 19:47:11 +08:00
linyuchen
b3aad8b0d9 fix: Check pic fil name ext 2024-05-15 19:19:10 +08:00
linyuchen
1489c6df25 feat: New face 2024-05-15 18:47:38 +08:00
linyuchen
2e225045e6 feat: Get cookies support domain 2024-05-15 17:57:15 +08:00
手瓜一十雪
11ed06148c fix: checkVersion Mirror 2024-05-14 14:43:52 +08:00
linyuchen
a3fc018186 fix: Compatible with win7 2024-05-12 20:36:27 +08:00
linyuchen
9692bf6ec6 refactor: Rename native node module dirname 2024-05-11 14:56:01 +08:00
linyuchen
9b3916307a fix: All images are the first image in single msg
fix: remote rkey
2024-05-11 14:52:59 +08:00
linyuchen
fdf96b479c Merge branch 'main' into dev
# Conflicts:
#	src/ntqqapi/external/cpmodule.ts
#	src/ntqqapi/external/crychic/index.ts
#	src/ntqqapi/external/moehook/hook.ts
#	src/onebot11/action/msg/SendMsg.ts
#	tsconfig.json
2024-05-10 20:28:44 +08:00
linyuchen
25c7a6096d refactor: path alias
fix: moehook
2024-05-10 20:23:30 +08:00
student_2333
627955e7fd chore: format 2024-05-10 13:34:49 +08:00
student_2333
43e9b070a9 fix: try 2 fix cannot parse msg err 2024-05-10 13:33:48 +08:00
linyuchen
78bb36a2bb fix: Music sign return null then throw exception 2024-05-07 17:46:47 +08:00
linyuchen
58e6e3cbda fix: Music sign return null then throw exception 2024-05-07 17:39:44 +08:00
linyuchen
1da086ce0a chore: v3.24.2 2024-05-05 20:20:30 +08:00
linyuchen
e9d43a9449 fix: http download filename special character 2024-05-05 20:06:07 +08:00
linyuchen
ce31052661 refactor: OB11Message add message_seq filed 2024-05-05 19:42:48 +08:00
linyuchen
3fd9b0a183 fix: 表情回应兼容int类型的emoji_id 2024-05-05 13:07:07 +08:00
linyuchen
7e1dee8e07 fix: msg db cache missing shortId 2024-05-04 23:35:19 +08:00
linyuchen
f2854fdf00 fix: report self recall twice 2024-05-04 20:30:39 +08:00
linyuchen
1fad95a55b chore: Version 3.24.1 2024-05-04 11:34:41 +08:00
linyuchen
5342e1521c Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/ntqqapi/external/moehook/MoeHoo-linux-x64.node
2024-05-03 21:26:31 +08:00
student_2333
3c532526df chore: sync external files 2024-05-01 15:25:49 +08:00
student_2333
05c6cae86f fix: reference before define 2024-05-01 11:10:42 +08:00
53 changed files with 4890 additions and 509 deletions

2
.gitignore vendored
View File

@@ -3,4 +3,4 @@ package-lock.json
dist/ dist/
out/ out/
.idea/ .idea/
.DS_Store .DS_Store

View File

@@ -1,5 +1,6 @@
import cp from 'vite-plugin-cp' import cp from 'vite-plugin-cp'
import './scripts/gen-version' import './scripts/gen-version'
import path from 'node:path'
const external = [ const external = [
'silk-wasm', 'silk-wasm',
@@ -34,6 +35,7 @@ let config = {
}, },
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg', './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
}, },
}, },
@@ -43,9 +45,9 @@ let config = {
...external.map(genCpModule), ...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' }, { src: './manifest.json', dest: 'dist' },
{ src: './icon.jpg', dest: 'dist' }, { src: './icon.jpg', dest: 'dist' },
{ src: './src/ntqqapi/external/crychic/crychic-win32-x64.node', dest: 'dist/main/' }, { src: './src/ntqqapi/native/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
{ src: './src/ntqqapi/external/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' }, // { src: './src/ntqqapi/native/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
{ src: './src/ntqqapi/external/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' }, // { src: './src/ntqqapi/native/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
], ],
}), }),
], ],

View File

@@ -1,10 +1,10 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.24.0", "name": "LLOneBot v3.24.4",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新", "description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
"version": "3.24.0", "version": "3.24.4",
"icon": "./icon.jpg", "icon": "./icon.jpg",
"authors": [ "authors": [
{ {

View File

@@ -10,7 +10,8 @@
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/", "deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/",
"build-win": "npm run build && npm run deploy-win", "build-win": "npm run build && npm run deploy-win",
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"", "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"",
"format": "prettier -cw ." "format": "prettier -cw .",
"check": "tsc"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",

View File

@@ -82,9 +82,12 @@ export class ConfigUtil {
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8') fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8')
} }
private checkOldConfig(currentConfig: Config | OB11Config, private checkOldConfig(
oldConfig: Config | OB11Config, currentConfig: Config | OB11Config,
currentKey: string, oldKey: string) { oldConfig: Config | OB11Config,
currentKey: string,
oldKey: string,
) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey] const oldValue = oldConfig[oldKey]
if (oldValue) { if (oldValue) {

View File

@@ -4,6 +4,7 @@ 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: '',
@@ -11,6 +12,10 @@ 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 let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
@@ -27,13 +32,13 @@ export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString()) let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend) { if (!friend) {
try { try {
const _friends = (await NTQQFriendApi.getFriends(true)) const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find(friend => friend[filterKey] === filterValue.toString()) friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
if (friend){ if (friend) {
friends.push(friend) friends.push(friend)
} }
} catch (e) { } catch (e) {
log("刷新好友列表失败", e.stack.toString()) log('刷新好友列表失败', e.stack.toString())
} }
} }
return friend return friend
@@ -48,8 +53,7 @@ export async function getGroup(qq: string): Promise<Group | undefined> {
if (group) { if (group) {
groups.push(group) groups.push(group)
} }
} catch (e) { } catch (e) {}
}
} }
return group return group
} }

View File

@@ -49,7 +49,7 @@ class DBUtil {
setTimeout(initDB, 300) setTimeout(initDB, 300)
} }
} }
initDB() setTimeout(initDB)
}).then() }).then()
const expiredMilliSecond = 1000 * 60 * 60 const expiredMilliSecond = 1000 * 60 * 60
@@ -154,12 +154,12 @@ class DBUtil {
this.updateMsg(msg).then() this.updateMsg(msg).then()
return existMsg.msgShortId return existMsg.msgShortId
} }
this.addCache(msg)
const shortMsgId = await this.genMsgShortId() const shortMsgId = await this.genMsgShortId()
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
msg.msgShortId = shortMsgId msg.msgShortId = shortMsgId
this.addCache(msg)
// log("新增消息记录", msg.msgId) // log("新增消息记录", msg.msgId)
this.db.put(shortIdKey, msg.msgId).then().catch() this.db.put(shortIdKey, msg.msgId).then().catch()
this.db.put(longIdKey, JSON.stringify(msg)).then().catch() this.db.put(longIdKey, JSON.stringify(msg)).then().catch()

View File

@@ -12,7 +12,7 @@ export interface OB11Config {
enableHttpHeart?: boolean enableHttpHeart?: boolean
} }
export interface CheckVersion { export interface CheckVersion {
result: boolean, result: boolean
version: string version: string
} }
export interface Config { export interface Config {

View File

@@ -0,0 +1,75 @@
import path from 'node:path'
import fs from 'node:fs'
import os from 'node:os'
import { systemPlatform } from './system'
export const exePath = process.execPath;
export const pkgInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'package.json');
let configVersionInfoPath;
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json');
} else {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './.config/QQ');
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
}
if (typeof configVersionInfoPath !== 'string') {
throw new Error('Something went wrong when load QQ info path');
}
export { configVersionInfoPath };
type QQPkgInfo = {
version: string;
buildVersion: string;
platform: string;
eleArch: string;
}
type QQVersionConfigInfo = {
baseVersion: string;
curVersion: string;
prevVersion: string;
onErrorVersions: Array<any>;
buildId: string;
}
let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.9-23361',
'curVersion': '9.9.9-23361',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '23361'
};
if (fs.existsSync(configVersionInfoPath)) {
try {
const _ =JSON.parse(fs.readFileSync(configVersionInfoPath).toString());
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _);
} catch (e) {
console.error('Load QQ version config info failed, Use default version', e);
}
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo;
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath);
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.9-23159',
// qua: 'V1_WIN_NQ_9.9.9_23159_GW_B',
// appid: '537213764',
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
let _appid: string = '537213803'; // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') {
_appid = '537213827';
}
// todo: mac 平台的 appid
export const appid = _appid;
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

View File

@@ -109,7 +109,7 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
} }
} }
} }
const fetchRes = await net.fetch(url, headers) const fetchRes = await net.fetch(url, {headers})
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`) if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
const blob = await fetchRes.blob() const blob = await fetchRes.blob()
@@ -176,6 +176,7 @@ export async function uri2local(uri: string, fileName: string = null): Promise<U
// res.ext = pathInfo.ext // res.ext = pathInfo.ext
} }
} }
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_')
res.fileName = fileName res.fileName = fileName
filePath = path.join(TEMP_DIR, uuidv4() + fileName) filePath = path.join(TEMP_DIR, uuidv4() + fileName)
fs.writeFileSync(filePath, buffer) fs.writeFileSync(filePath, buffer)

View File

@@ -65,3 +65,33 @@ export function wrapText(str: string, maxLength: number): string {
return result return result
} }
/**
* 函数缓存装饰器根据方法名、参数、自定义key生成缓存键在一定时间内返回缓存结果
* @param ttl 超时时间,单位毫秒
* @param customKey 自定义缓存键前缀,可为空,防止方法名参数名一致时导致缓存键冲突
* @returns 处理后缓存或调用原方法的结果
*/
export function cacheFunc(ttl: number, customKey: string='') {
const cache = new Map<string, { expiry: number; value: any }>();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value;
const className = target.constructor.name; // 获取类名
const methodName = propertyKey; // 获取方法名
descriptor.value = async function (...args: any[]){
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
return cached.value;
} else {
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl });
return result;
}
};
return descriptor;
};
}

View File

@@ -5,7 +5,7 @@ export * from './file'
export * from './helper' export * from './helper'
export * from './log' export * from './log'
export * from './qqlevel' export * from './qqlevel'
export * from './qqpkg' export * from './QQBasicInfo'
export * from './upgrade' export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR = path.join(DATA_DIR, 'temp') export const TEMP_DIR = path.join(DATA_DIR, 'temp')
@@ -16,3 +16,4 @@ if (!fs.existsSync(TEMP_DIR)) {
export { getVideoInfo } from './video' export { getVideoInfo } from './video'
export { checkFfmpeg } from './video' export { checkFfmpeg } from './video'
export { encodeSilk } from './audio' export { encodeSilk } from './audio'
export { isQQ998 } from './QQBasicInfo'

View File

@@ -1,12 +0,0 @@
import path from 'path'
type QQPkgInfo = {
version: string
buildVersion: string
platform: string
eleArch: string
}
export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, 'app/package.json'))
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

View File

@@ -0,0 +1,87 @@
import https from 'node:https';
import http from 'node:http';
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<Map<string, string>> {
return new Promise<Map<string, string>>((resolve, reject) => {
const protocol = url.startsWith('https://') ? https : http;
protocol.get(url, (res) => {
const cookiesHeader = res.headers['set-cookie'];
if (!cookiesHeader) {
resolve(new Map<string, string>());
} else {
const cookiesMap = new Map<string, string>();
cookiesHeader.forEach((cookieStr) => {
cookieStr.split(';').forEach((cookiePart) => {
const trimmedPart = cookiePart.trim();
if (trimmedPart.includes('=')) {
const [key, value] = trimmedPart.split('=').map(part => part.trim());
cookiesMap.set(key, decodeURIComponent(value)); // 解码cookie值
}
});
});
resolve(cookiesMap);
}
}).on('error', (error) => {
reject(error);
});
});
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
let option = new URL(url);
const protocol = url.startsWith('https://') ? https : http;
const options = {
hostname: option.hostname,
port: option.port,
path: option.href,
method: method,
headers: headers
};
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: any) => {
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
});
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
if (isJsonRet) {
const responseJson = JSON.parse(responseBody);
resolve(responseJson as T);
} else {
resolve(responseBody as T);
}
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
} catch (parseError) {
reject(parseError);
}
});
});
req.on('error', (error: any) => {
reject(error);
});
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
if (isArgJson) {
req.write(JSON.stringify(data));
} else {
req.write(data);
}
}
req.end();
});
}
// 请求返回都是原始内容
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
}
}

View File

@@ -0,0 +1,10 @@
import os from 'node:os';
import path from 'node:path';
export const systemPlatform = os.platform();
export const cpuArch = os.arch();
export const systemVersion = os.release();
// export const hostname = os.hostname(); // win7不支持
const homeDir = os.homedir();
export const downloadsPath = path.join(homeDir, 'Downloads');
export const systemName = os.type();

View File

@@ -5,7 +5,7 @@ import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.'
import compressing from 'compressing' import compressing from 'compressing'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/'] const downloadMirrorHosts = ['https://mirror.ghproxy.com/']
const checkVersionMirrorHosts = ['https://521github.com'] const checkVersionMirrorHosts = ['https://kkgithub.com']
export async function checkNewVersion() { export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion() const latestVersionText = await getRemoteVersion()

View File

@@ -48,13 +48,14 @@ import { dbUtil } from '../common/db'
import { setConfig } from './setConfig' import { setConfig } from './setConfig'
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 { crychic } from '../ntqqapi/external/crychic' import { crychic } from '../ntqqapi/native/crychic'
import { OB11FriendPokeEvent, OB11GroupPokeEvent } from '../onebot11/event/notice/OB11PokeEvent' import { OB11FriendPokeEvent, OB11GroupPokeEvent } from '../onebot11/event/notice/OB11PokeEvent'
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/native/wrapper'
let running = false let running = false
@@ -224,39 +225,28 @@ function onLoad() {
log('report message error: ', e.stack.toString()) log('report message error: ', e.stack.toString())
} }
}) })
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("message update", message) log('message update', message.msgId, message)
if (message.recallTime != '0') { if (message.recallTime != '0') {
//todo: 这个判断方法不太好,应该使用灰色消息元素来判断 if (recallMsgIds.includes(message.msgId)) {
// 撤回消息上报 continue
}
recallMsgIds.push(message.msgId)
const oriMessage = await dbUtil.getMsgByLongId(message.msgId) const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) { if (!oriMessage) {
continue continue
} }
oriMessage.recallTime = message.recallTime oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then() dbUtil.updateMsg(oriMessage).then()
if (message.chatType == ChatType.friend) { message.msgShortId = oriMessage.msgShortId
const friendRecallEvent = new OB11FriendRecallNoticeEvent( OB11Constructor.RecallEvent(message).then((recallEvent) => {
parseInt(message.senderUin), if (recallEvent) {
oriMessage.msgShortId, log('post recall event', recallEvent)
) postOB11Event(recallEvent)
postOB11Event(friendRecallEvent)
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin
} }
const groupRecallEvent = new OB11GroupRecallNoticeEvent( })
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId,
)
postOB11Event(groupRecallEvent)
}
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了 // 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue continue
} }
@@ -311,7 +301,11 @@ function onLoad() {
// 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);
// } // }
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(notify.type)) { if (
[GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
notify.type,
)
) {
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid) const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
log('有管理员变动通知') log('有管理员变动通知')
refreshGroupMembers(notify.group.groupCode).then() refreshGroupMembers(notify.group.groupCode).then()
@@ -321,7 +315,12 @@ function onLoad() {
if (member1) { if (member1) {
log('变动管理员获取成功') log('变动管理员获取成功')
groupAdminNoticeEvent.user_id = parseInt(member1.uin) groupAdminNoticeEvent.user_id = parseInt(member1.uin)
groupAdminNoticeEvent.sub_type = [GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(notify.type) ? 'unset' : 'set' groupAdminNoticeEvent.sub_type = [
GroupNotifyTypes.ADMIN_UNSET,
GroupNotifyTypes.ADMIN_UNSET_OTHER,
].includes(notify.type)
? 'unset'
: 'set'
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal; // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
postOB11Event(groupAdminNoticeEvent, true) postOB11Event(groupAdminNoticeEvent, true)
} else { } else {

View File

@@ -7,26 +7,23 @@ import {
ChatCacheList, ChatCacheList,
ChatCacheListItemBasic, ChatCacheListItemBasic,
ChatType, ChatType,
ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, RawMessage, ElementType,
IMAGE_HTTP_HOST,
IMAGE_HTTP_HOST_NT, PicElement,
RawMessage,
} from '../types' } from '../types'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log } from '../../common/utils' import { log } from '@/common/utils'
import https from 'https' import { rkeyManager } from '@/ntqqapi/api/rkey'
import { sleep } from '../../common/utils'
import { hookApi } from '../external/moehook/hook'
let privateImageRKey = ''
let groupImageRKey = ''
let lastGetPrivateRKeyTime = 0
let lastGetGroupRKeyTime = 0
const rkeyExpireTime = 1000 * 60 * 30
export class NTQQFileApi { export class NTQQFileApi {
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath], className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_TYPE,
args: [filePath],
}) })
} }
@@ -42,16 +39,20 @@ export class NTQQFileApi {
return await callNTQQApi<string>({ return await callNTQQApi<string>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY, methodName: NTQQApiMethod.FILE_COPY,
args: [{ args: [
fromPath: filePath, {
toPath: destPath, fromPath: filePath,
}], toPath: destPath,
},
],
}) })
} }
static async getFileSize(filePath: string) { static async getFileSize(filePath: string) {
return await callNTQQApi<number>({ return await callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath], className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_SIZE,
args: [filePath],
}) })
} }
@@ -70,18 +71,20 @@ export class NTQQFileApi {
} }
const mediaPath = await callNTQQApi<string>({ const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH, methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{ args: [
path_info: { {
md5HexStr: md5, path_info: {
fileName: fileName, md5HexStr: md5,
elementType: elementType, fileName: fileName,
elementSubType, elementType: elementType,
thumbSize: 0, elementSubType,
needCreate: true, thumbSize: 0,
downloadType: 1, needCreate: true,
file_uuid: '', downloadType: 1,
file_uuid: '',
},
}, },
}], ],
}) })
log('media path', mediaPath) log('media path', mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath) await NTQQFileApi.copyFile(filePath, mediaPath)
@@ -95,7 +98,15 @@ export class NTQQFileApi {
} }
} }
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, force: boolean = false) { static async downloadMedia(
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbPath: string,
sourcePath: string,
force: boolean = false,
) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
if (force) { if (force) {
@@ -126,7 +137,7 @@ export class NTQQFileApi {
methodName: NTQQApiMethod.DOWNLOAD_MEDIA, methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams, args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => { cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => {
log('media 下载完成判断', payload.notifyInfo.msgId, msgId) log('media 下载完成判断', payload.notifyInfo.msgId, msgId)
return payload.notifyInfo.msgId == msgId return payload.notifyInfo.msgId == msgId
}, },
@@ -135,21 +146,19 @@ export class NTQQFileApi {
} }
static async getImageSize(filePath: string) { static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number, height: number }>({ return await callNTQQApi<{ width: number; height: number }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath], className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.IMAGE_SIZE,
args: [filePath],
}) })
} }
static async getImageUrl(msg: RawMessage) { static async getImageUrl(picElement: PicElement, chatType: ChatType) {
const isPrivateImage = msg.chatType !== ChatType.group const isPrivateImage = chatType !== ChatType.group
const msgElement = msg.elements.find(e => !!e.picElement) const url = picElement.originImageUrl // 没有域名
if (!msgElement) { const md5HexStr = picElement.md5HexStr
return '' const fileMd5 = picElement.md5HexStr
} const fileUuid = picElement.fileUuid
const url = msgElement.picElement.originImageUrl // 没有域名
const md5HexStr = msgElement.picElement.md5HexStr
const fileMd5 = msgElement.picElement.md5HexStr
const fileUuid = msgElement.picElement.fileUuid
if (url) { if (url) {
if (url.startsWith('/download')) { if (url.startsWith('/download')) {
// console.log('rkey', rkey); // console.log('rkey', rkey);
@@ -157,70 +166,9 @@ export class NTQQFileApi {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
} }
if (!hookApi.isAvailable()) { const rkeyData = await rkeyManager.getRkey();
log('hookApi is not available') const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey;
return '' return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
}
const saveRKey = (rkey: string) => {
if (isPrivateImage) {
privateImageRKey = rkey
lastGetPrivateRKeyTime = Date.now()
} else {
groupImageRKey = rkey
lastGetGroupRKeyTime = Date.now()
}
}
const refreshRKey = async () => {
log('获取图片rkey...')
NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, msgElement.elementId, '', msgElement.picElement.sourcePath, false).then().catch(() => {
})
await sleep(1000)
const _rkey = hookApi.getRKey()
if (_rkey) {
const imageUrl = IMAGE_HTTP_HOST_NT + url + _rkey
// 验证_rkey是否有效
try {
await new Promise((res, rej) => {
https.get(imageUrl, response => {
if (response.statusCode !== 200) {
rej('图片rkey获取失败')
} else {
res(response)
}
}).on('error', e => {
rej(e)
})
})
log('图片rkey获取成功', _rkey)
saveRKey(_rkey)
return _rkey
}catch (e) {
log('图片rkey有误', imageUrl)
}
}
}
const existsRKey = isPrivateImage ? privateImageRKey : groupImageRKey
const lastGetRKeyTime = isPrivateImage ? lastGetPrivateRKeyTime : lastGetGroupRKeyTime
if ((Date.now() - lastGetRKeyTime > rkeyExpireTime)) {
// rkey过期
const newRKey = await refreshRKey()
if (newRKey) {
return IMAGE_HTTP_HOST_NT + url + `${newRKey}`
} else {
log('图片rkey获取失败', url)
if(existsRKey){
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
}
return ''
}
}
// 使用未过期的rkey
if (existsRKey) {
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
}
} else { } else {
// 老的图片url不需要rkey // 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url return IMAGE_HTTP_HOST + url
@@ -229,47 +177,58 @@ export class NTQQFileApi {
// 没有url需要自己拼接 // 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
} }
log('图片url获取失败', msg) log('图片url获取失败', picElement)
return '' return ''
} }
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) { static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE, methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{ args: [
isSilent, {
}, null], isSilent,
},
null,
],
}) })
} }
static getCacheSessionPathList() { static getCacheSessionPathList() {
return callNTQQApi<{ return callNTQQApi<
key: string, {
value: string key: string
}[]>({ value: string
}[]
>({
className: NTQQApiClass.OS_API, className: NTQQApiClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION, methodName: NTQQApiMethod.CACHE_PATH_SESSION,
}) })
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么 return callNTQQApi<any>({
// TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR, methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{ args: [
keys: cacheKeys, {
}, null], keys: cacheKeys,
},
null,
],
}) })
} }
static addCacheScannedPaths(pathMap: object = {}) { static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({ return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{ args: [
pathMap: { ...pathMap }, {
}, null], pathMap: { ...pathMap },
},
null,
],
}) })
} }
@@ -303,14 +262,18 @@ export class NTQQFileCacheApi {
return new Promise<ChatCacheList>((res, rej) => { return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({ callNTQQApi<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET, methodName: NTQQApiMethod.CACHE_CHAT_GET,
args: [{ args: [
chatType: type, {
pageSize, chatType: type,
order: 1, pageSize,
pageIndex, order: 1,
}, null], pageIndex,
}).then(list => res(list)) },
.catch(e => rej(e)) null,
],
})
.then((list) => res(list))
.catch((e) => rej(e))
}) })
} }
@@ -319,24 +282,29 @@ export class NTQQFileCacheApi {
return callNTQQApi<CacheFileList>({ return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET, methodName: NTQQApiMethod.CACHE_FILE_GET,
args: [{ args: [
fileType: fileType, {
restart: true, fileType: fileType,
pageSize: pageSize, restart: true,
order: 1, pageSize: pageSize,
lastRecord: _lastRecord, order: 1,
}, null], lastRecord: _lastRecord,
},
null,
],
}) })
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{ args: [
chats, {
fileKeys, chats,
}, null], fileKeys,
},
null,
],
}) })
} }
} }

View File

@@ -74,7 +74,11 @@ export class NTQQGroupApi {
} }
static async getGroupIgnoreNotifies() { static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies() await NTQQGroupApi.getGroupNotifies()
return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow, [], ReceiveCmdS.GROUP_NOTIFY) return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow,
[],
ReceiveCmdS.GROUP_NOTIFY,
)
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq) const notify: GroupNotify = await dbUtil.getGroupNotify(seq)

View File

@@ -20,6 +20,7 @@ export class NTQQMsgApi {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString()
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.EMOJI_LIKE, methodName: NTQQApiMethod.EMOJI_LIKE,
args: [ args: [

59
src/ntqqapi/api/rkey.ts Normal file
View File

@@ -0,0 +1,59 @@
//远端rkey获取
import { log } from '@/common/utils'
interface ServerRkeyData{
group_rkey: string;
private_rkey: string;
expired_time: number;
}
class RkeyManager {
serverUrl: string = '';
private rkeyData: ServerRkeyData = {
group_rkey: '',
private_rkey: '',
expired_time: 0
};
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
}
async getRkey(){
if (this.isExpired()) {
try {
await this.refreshRkey();
} catch (e) {
log('获取rkey失败', e);
}
}
return this.rkeyData;
}
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(): Promise<any> {
//刷新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);
});
});
}
}
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey');

View File

@@ -3,10 +3,19 @@ import { Group, SelfInfo, User } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { selfInfo, uidMaps } from '../../common/data' import { selfInfo, uidMaps } from '../../common/data'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindowApi, NTQQWindows } from './window'
import { isQQ998, log, sleep } from '../../common/utils' import { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import * as https from 'https'
let userInfoCache: Record<string, User> = {} // uid: User let 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>({
@@ -28,6 +37,7 @@ export class NTQQUserApi {
timeoutSecond: 2, timeoutSecond: 2,
}) })
} }
static async getUserInfo(uid: string) { static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({ const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO, methodName: NTQQApiMethod.USER_INFO,
@@ -36,6 +46,7 @@ export class NTQQUserApi {
}) })
return result.profiles.get(uid) return result.profiles.get(uid)
} }
static async getUserDetailInfo(uid: string, getLevel = false) { static async getUserDetailInfo(uid: string, getLevel = false) {
// this.getUserInfo(uid); // this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
@@ -84,64 +95,46 @@ export class NTQQUserApi {
], ],
}) })
} }
static async getSkey(groupName: string, groupCode: string): Promise<{ data: string }> {
return await NTQQWindowApi.openWindow<{ data: string }>( static async getSkey(): Promise<string> {
NTQQWindows.GroupHomeWorkWindow, const clientKeyData = await this.getClientKey()
[ if (clientKeyData.result !== 0) {
{ throw new Error('获取clientKey失败')
groupName, }
groupCode, const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
source: 'funcbar', + '&clientkey=' + clientKeyData.clientKey
}, + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
],
ReceiveCmdS.SKEY_UPDATE, return new Promise((resolve, reject) => {
1, const req = https.get(url, (res) => {
) const rawCookies = res.headers['set-cookie']
// return await callNTQQApi<string>({ const cookies = {}
// className: NTQQApiClass.GROUP_HOME_WORK, rawCookies.forEach(cookie => {
// methodName: NTQQApiMethod.UPDATE_SKEY, // 使用正则表达式匹配 cookie 名称和值
// args: [ const regex = /([^=;]+)=([^;]*)/
// { const match = regex.exec(cookie)
// domain: "qun.qq.com" if (match) {
// } cookies[match[1].trim()] = match[2].trim()
// ] }
// }) })
// return await callNTQQApi<GeneralCallResult>({ resolve(cookies['skey'])
// methodName: NTQQApiMethod.GET_SKEY, })
// args: [ req.on('error', e => {
// { reject(e)
// "domains": [ });
// "qzone.qq.com", req.end();
// "qlive.qq.com", });
// "qun.qq.com",
// "gamecenter.qq.com",
// "vip.qq.com",
// "qianbao.qq.com",
// "qidian.qq.com"
// ],
// "isForNewPCQQ": false
// },
// null
// ]
// })
} }
static async getCookie(group: Group) { @cacheFunc(60 * 30 * 1000)
let cookies = await this.getCookieWithoutSkey() static async getCookies(domain: string) {
let skey = '' const skey = await this.getSkey();
for (let i = 0; i < 2; i++) { const pskey= (await this.getPSkey([domain])).get(domain);
skey = (await this.getSkey(group.groupName, group.groupCode)).data if (!pskey || !skey) {
skey = skey.trim() throw new Error('获取Cookies失败')
if (skey) {
break
}
await sleep(1000)
}
if (!skey) {
throw new Error('获取skey失败')
} }
const bkn = NTQQUserApi.genBkn(skey) const bkn = NTQQUserApi.genBkn(skey)
cookies = cookies.replace('skey=;', `skey=${skey};`) const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`;
return { cookies, bkn } return { cookies, bkn }
} }
@@ -156,4 +149,17 @@ export class NTQQUserApi {
return (hash & 0x7fffffff).toString() return (hash & 0x7fffffff).toString()
} }
static async getPSkey(domains: string[]): Promise<Map<string, string>> {
const res = await wrapperApi.NodeIQQNTWrapperSession.getTipOffService().getPskey(domains, true)
if (res.result !== 0) {
throw new Error(`获取Pskey失败: ${res.errMsg}`)
}
return res.domainPskeyMap
}
static async getClientKey(): Promise<ClientKeyData> {
return await wrapperApi.NodeIQQNTWrapperSession.getTicketService().forceFetchClientKey('')
}
} }

View File

@@ -1,76 +1,366 @@
import { groups } from '../../common/data' import { WebGroupData, groups, selfInfo } from '@/common/data';
import { log } from '../../common/utils' import { log } from '@/common/utils/log';
import { NTQQUserApi } from './user' import { NTQQUserApi } from './user';
import { RequestUtil } from '@/common/utils/request';
export class WebApi { export enum WebHonorType {
private static bkn: string ALL = 'all',
private static skey: string TALKACTIVE = 'talkative',
private static pskey: string PERFROMER = 'performer',
private static cookie: string LEGEND = 'legend',
private defaultHeaders: Record<string, string> = { STORONGE_NEWBI = 'strong_newbie',
'User-Agent': 'QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0', EMOTION = 'emotion'
}
export interface WebApiGroupMember {
uin: number
role: number
g: number
join_time: number
last_speak_time: number
lv: {
point: number
level: number
} }
card: string
constructor() {} tags: string
flag: number
public async addGroupDigest(groupCode: string, msgSeq: string) { nick: 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` qage: number
const res = await this.request(url) rm: number
return await res.json() }
interface WebApiGroupMemberRet {
ec: number
errcode: number
em: string
cache: number
adm_num: number
levelname: any
mems: WebApiGroupMember[]
count: number
svr_time: number
max_count: number
search_count: number
extmode: number
}
export interface WebApiGroupNoticeFeed {
u: number//发送者
fid: string//fid
pubt: number//时间
msg: {
text: string
text_face: string
title: string,
pics?: {
id: string,
w: string,
h: string
}[]
} }
type: number
public async getGroupDigest(groupCode: string) { fn: number
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20` cn: number
const res = await this.request(url) vn: number
log(res.headers) settings: {
return await res.json() is_show_edit_card: number
remind_ts: number
tip_window_type: number
confirm_required: number
} }
read_num: number
private genBkn(sKey: string) { is_read: number
return NTQQUserApi.genBkn(sKey) is_all_confirm: number
}
export interface WebApiGroupNoticeRet {
ec: number
em: string
ltsm: number
srv_code: number
read_only: number
role: number
feeds: WebApiGroupNoticeFeed[]
group: {
group_id: number
class_ext: number
} }
private async init() { sta: number,
if (!WebApi.bkn) { gln: number
const group = groups[0] tst: number,
WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data ui: any
WebApi.bkn = this.genBkn(WebApi.skey) server_time: number
let cookie = await NTQQUserApi.getCookieWithoutSkey() svrt: number
const pskeyRegex = /p_skey=([^;]+)/ ad: number
const match = cookie.match(pskeyRegex) }
const pskeyValue = match ? match[1] : null interface GroupEssenceMsg {
WebApi.pskey = pskeyValue group_code: string
if (cookie.indexOf('skey=;') !== -1) { msg_seq: number
cookie = cookie.replace('skey=;', `skey=${WebApi.skey};`) msg_random: number
} sender_uin: string
WebApi.cookie = cookie sender_nick: string
// for(const kv of WebApi.cookie.split(";")){ sender_time: number
// const [key, value] = kv.split("="); add_digest_uin: string
// } add_digest_nick: string
// log("set cookie", key, value) add_digest_time: number
// await session.defaultSession.cookies.set({ msg_content: any[]
// url: 'https://qun.qq.com', // 你要请求的域名 can_be_removed: true
// name: key.trim(), }
// value: value.trim(), export interface GroupEssenceMsgRet {
// expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间例如设置为当前时间之后的300秒 retcode: number
// }); retmsg: string
// } data: {
} msg_list: GroupEssenceMsg[]
} is_end: boolean
group_role: number
private async request(url: string, method: 'GET' | 'POST' = 'GET', headers: Record<string, string> = {}) { config_page_url: string
await this.init() }
url += '&bkn=' + WebApi.bkn }
let _headers: Record<string, string> = { export class WebApi {
...this.defaultHeaders, static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet> {
...headers, const {cookies: CookieValue, bkn: Bkn} = (await NTQQUserApi.getCookies('qun.qq.com'))
Cookie: WebApi.cookie, const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20';
credentials: 'include', let ret;
} try {
log('request', url, _headers) ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue });
const options = { } catch {
method: method, return undefined;
headers: _headers, }
} //console.log(url, CookieValue);
return fetch(url, options) if (ret.retcode !== 0) {
return undefined;
}
return ret;
}
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode);
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
try {
let cachedData = WebGroupData.GroupData.get(GroupCode);
let cachedTime = WebGroupData.GroupTime.get(GroupCode);
if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
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 {
MemberData = cachedData as Array<WebApiGroupMember>;
}
} catch {
return MemberData;
}
return MemberData;
}
// 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 res = await this.request(url);
// return await res.json();
// }
// public async getGroupDigest(groupCode: string) {
// const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`;
// const res = await this.request(url);
// return await res.json();
// }
static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: any = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
}
const Bkn = WebApi.genBkn(_Skey);
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}';
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn;
try {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue });
return ret;
} catch (e) {
return undefined;
}
return undefined;
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: WebApiGroupNoticeRet | undefined = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
}
const Bkn = WebApi.genBkn(_Skey);
const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20';
try {
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue });
if (ret?.ec !== 0) {
return undefined;
}
return ret;
} catch (e) {
return undefined;
}
return undefined;
}
static genBkn(sKey: string) {
sKey = sKey || '';
let hash = 5381;
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
}
return (hash & 0x7FFFFFFF).toString();
}
//实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
let res = '';
let resJson;
try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue });
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/);
if (match) {
resJson = JSON.parse(match[1].trim());
}
if (Internal_type === 1) {
return resJson?.talkativeList;
} else {
return resJson?.actorList;
}
} catch (e) {
log('获取当前群荣耀失败', url, e);
}
return undefined;
}
let HonorInfo: any = { group_id: groupCode };
const CookieValue = (await NTQQUserApi.getCookies('qun.qq.com')).cookies;
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 1);
if (!RetInternal) {
throw new Error('获取龙王信息失败');
}
HonorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
avatar: RetInternal[0]?.avatar,
nickname: RetInternal[0]?.name,
day_count: 0,
description: RetInternal[0]?.desc
}
HonorInfo.talkative_list = [];
for (const talkative_ele of RetInternal) {
HonorInfo.talkative_list.push({
user_id: talkative_ele?.uin,
avatar: talkative_ele?.avatar,
description: talkative_ele?.desc,
day_count: 0,
nickname: talkative_ele?.name
});
}
} catch (e) {
log(e);
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 2);
if (!RetInternal) {
throw new Error('获取群聊之火失败');
}
HonorInfo.performer_list = [];
for (const performer_ele of RetInternal) {
HonorInfo.performer_list.push({
user_id: performer_ele?.uin,
nickname: performer_ele?.name,
avatar: performer_ele?.avatar,
description: performer_ele?.desc
});
}
} catch (e) {
log(e);
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 3);
if (!RetInternal) {
throw new Error('获取群聊炽焰失败');
}
HonorInfo.legend_list = [];
for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({
user_id: legend_ele?.uin,
nickname: legend_ele?.name,
avatar: legend_ele?.avatar,
desc: legend_ele?.description
});
}
} catch (e) {
log('获取群聊炽焰失败', e);
}
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 6);
if (!RetInternal) {
throw new Error('获取快乐源泉失败');
}
HonorInfo.emotion_list = [];
for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({
user_id: emotion_ele?.uin,
nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar,
desc: emotion_ele?.description
});
}
} catch (e) {
log('获取快乐源泉失败', e);
}
}
//冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
HonorInfo.strong_newbie_list = [];
}
return HonorInfo;
} }
} }

View File

@@ -2,7 +2,6 @@ import {
AtType, AtType,
ElementType, ElementType,
FaceIndex, FaceIndex,
FaceType,
PicType, PicType,
SendArkElement, SendArkElement,
SendFaceElement, SendFaceElement,
@@ -22,8 +21,9 @@ import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils' import { isNull } from '../common/utils'
import faceConfig from './face_config.json';
export const mFaceCache = new Map<string, string>(); // emojiId -> faceName export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
static poke(groupCode: string, uin: string) { static poke(groupCode: string, uin: string) {
@@ -119,15 +119,15 @@ export class SendMsgElementConstructor {
} }
static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> { static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
try{ try {
await fs.stat(filePath) await fs.stat(filePath)
}catch (e) { } catch (e) {
throw `文件${filePath}异常,不存在` throw `文件${filePath}异常,不存在`
} }
log("复制视频到QQ目录", filePath) log('复制视频到QQ目录', filePath)
let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO) let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO)
log("复制视频到QQ目录完成", path) log('复制视频到QQ目录完成', path)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
@@ -265,13 +265,30 @@ export class SendMsgElementConstructor {
} }
static face(faceId: number): SendFaceElement { static face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji
const face = sysFaces.find((face) => face.QSid === faceId.toString())
faceId = parseInt(faceId.toString()) faceId = parseInt(faceId.toString())
// let faceType = parseInt(faceId.toString().substring(0, 1));
let faceType = 1
if (faceId >= 222){
faceType = 2
}
if (face.AniStickerType){
faceType = 3;
}
return { return {
elementType: ElementType.FACE, elementType: ElementType.FACE,
elementId: '', elementId: '',
faceElement: { faceElement: {
faceIndex: faceId, faceIndex: faceId,
faceType: faceId < 222 ? FaceType.normal : FaceType.normal2, faceType,
faceText: face.QDes,
stickerId: face.AniStickerId,
stickerType: face.AniStickerType,
packId: face.AniStickerPackId,
sourceType: 1,
}, },
} }
} }
@@ -298,7 +315,7 @@ export class SendMsgElementConstructor {
elementId: '', elementId: '',
faceElement: { faceElement: {
faceIndex: FaceIndex.dice, faceIndex: FaceIndex.dice,
faceType: FaceType.dice, faceType: 3,
faceText: '[骰子]', faceText: '[骰子]',
packId: '1', packId: '1',
stickerId: '33', stickerId: '33',

View File

@@ -1,54 +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()

3665
src/ntqqapi/face_config.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
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,11 +1,9 @@
import * as os from "os";
import fs from "fs";
import path from "node:path";
import {cpModule} from "../cpmodule"; import {cpModule} from "../cpmodule";
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface MoeHook { interface MoeHook {
GetRkey: () => string, // Return '&rkey=xxx' GetRkey: () => string, // Return '&rkey=xxx'
HookRkey: () => string HookRkey: (version: string) => string
} }
@@ -16,7 +14,8 @@ class HookApi {
cpModule('MoeHoo'); cpModule('MoeHoo');
try { try {
this.moeHook = require('./MoeHoo.node'); this.moeHook = require('./MoeHoo.node');
console.log("hook rkey地址", this.moeHook!.HookRkey()); console.log("hook rkey qq version", this.moeHook!.HookRkey(qqPkgInfo.version));
console.log("hook rkey地址", this.moeHook!.HookRkey(qqPkgInfo.version));
} catch (e) { } catch (e) {
console.log('加载 moehoo 失败', e); console.log('加载 moehoo 失败', e);
} }
@@ -31,4 +30,4 @@ class HookApi {
} }
} }
export const hookApi = new HookApi(); // export const hookApi = new HookApi();

View File

@@ -0,0 +1,19 @@
let Process = require('process')
let os = require('os')
Process.dlopenOrig = Process.dlopen
export const wrapperApi: any = {}
Process.dlopen = function(module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
let dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
let ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
},
})
}
return dlopenRet
}

View File

@@ -4,7 +4,6 @@ import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeR
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { log } from '../common/utils/log' import { log } from '../common/utils/log'
import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window' import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window'
import { WebApi } from './api/webapi'
import { HOOK_LOG } from '../common/config' import { HOOK_LOG } from '../common/config'
export enum NTQQApiClass { export enum NTQQApiClass {
@@ -21,6 +20,7 @@ export enum NTQQApiClass {
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
TEST = 'NodeIKernelTipOffService/getPskey',
RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact', RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息 ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息 ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
@@ -81,7 +81,7 @@ export enum NTQQApiMethod {
OPEN_EXTRA_WINDOW = 'openExternalWindow', OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_SKEY = 'nodeIKernelTipOffService/getPskey', GET_PSKEY = 'nodeIKernelTipOffService/getPskey',
UPDATE_SKEY = 'updatePskey', UPDATE_SKEY = 'updatePskey',
FETCH_UNITED_COMMEND_CONFIG = 'nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', // 发包需要调用的 FETCH_UNITED_COMMEND_CONFIG = 'nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', // 发包需要调用的

View File

@@ -188,6 +188,7 @@ export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn'
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn' export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export interface PicElement { export interface PicElement {
picType: PicType // 有这玩意儿吗
originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string originImageMd5?: string
sourcePath: string // 图片本地路径 sourcePath: string // 图片本地路径
@@ -201,6 +202,7 @@ export interface PicElement {
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
RECALL = 1,
INVITE_NEW_MEMBER = 12, INVITE_NEW_MEMBER = 12,
MEMBER_NEW_TITLE = 17, MEMBER_NEW_TITLE = 17,
} }
@@ -213,6 +215,8 @@ export interface GrayTipElement {
operatorNick: string operatorNick: string
operatorRemark: string operatorRemark: string
operatorMemRemark?: string operatorMemRemark?: string
origMsgSenderUid?: string
isSelfOperate?: boolean
wording: string // 自定义的撤回提示语 wording: string // 自定义的撤回提示语
} }
aioOpGrayTipElement: TipAioOpGrayTipElement aioOpGrayTipElement: TipAioOpGrayTipElement
@@ -226,11 +230,6 @@ export interface GrayTipElement {
} }
} }
export enum FaceType {
normal = 1, // 小黄脸
normal2 = 2, // 新小黄脸, 从faceIndex 222开始
dice = 3, // 骰子
}
export enum FaceIndex { export enum FaceIndex {
dice = 358, dice = 358,
@@ -239,7 +238,7 @@ export enum FaceIndex {
export interface FaceElement { export interface FaceElement {
faceIndex: number faceIndex: number
faceType: FaceType faceType: number
faceText?: string faceText?: string
packId?: string packId?: string
stickerId?: string stickerId?: string

View File

@@ -5,8 +5,8 @@ export enum GroupNotifyTypes {
ADMIN_SET = 8, ADMIN_SET = 8,
KICK_MEMBER = 9, KICK_MEMBER = 9,
MEMBER_EXIT = 11, // 主动退出 MEMBER_EXIT = 11, // 主动退出
ADMIN_UNSET = 12, // 我被取消管理员 ADMIN_UNSET = 12, // 我被取消管理员
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员 ADMIN_UNSET_OTHER = 13, // 其他人取消管理员
} }
export interface GroupNotifies { export interface GroupNotifies {

View File

@@ -1,12 +1,12 @@
import BaseAction from '../BaseAction' import BaseAction from '../BaseAction'
import fs from 'fs/promises' import fs from 'fs/promises'
import { dbUtil } from '../../../common/db' import { dbUtil } from '@/common/db'
import { getConfigUtil } from '../../../common/config' import { getConfigUtil } from '@/common/config'
import { log, sleep, uri2local } from '../../../common/utils' import { log, sleep, uri2local } from '@/common/utils'
import { NTQQFileApi } from '../../../ntqqapi/api/file' import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '../../../ntqqapi/types' import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types'
import { FileCache } from '../../../common/types' import { FileCache } from '@/common/types'
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名或者fileUuid file: string // 文件名或者fileUuid

View File

@@ -0,0 +1,24 @@
import { GroupEssenceMsgRet, WebApi } from "@/ntqqapi/api";
import BaseAction from "../BaseAction";
import { ActionName } from "../types";
interface PayloadType {
group_id: number;
pages?: number;
}
export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg;
protected async _handle(payload: PayloadType) {
throw '此 api 暂不支持'
const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages?.toString() || '0');
if (!ret) {
throw new Error('获取失败');
}
// ret.map((item) => {
//
// })
return ret;
}
}

View File

@@ -0,0 +1,22 @@
import { WebApi, WebHonorType } from "@/ntqqapi/api";
import { ActionName } from "../types";
import BaseAction from "../BaseAction";
interface Payload {
group_id: number,
type?: WebHonorType
}
export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo;
protected async _handle(payload: Payload) {
// console.log(await NTQQUserApi.getRobotUinRange());
if (!payload.group_id) {
throw '缺少参数group_id';
}
if (!payload.type) {
payload.type = WebHonorType.ALL;
}
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type);
}
}

View File

@@ -14,13 +14,10 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
if ( if (groups.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
groups.length === 0
|| payload?.no_cache === true || payload?.no_cache === 'true'
) {
try { try {
const groups = await NTQQGroupApi.getGroups(true) const groups = await NTQQGroupApi.getGroups(true)
log("强制刷新群列表, 数量:", groups.length) log('强制刷新群列表, 数量:', groups.length)
return OB11Constructor.groups(groups) return OB11Constructor.groups(groups)
} catch (e) {} } catch (e) {}
} }

View File

@@ -7,7 +7,7 @@ import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { log } from '../../../common/utils' import { log } from '../../../common/utils'
export interface PayloadType { export interface PayloadType {
group_id: number, group_id: number
no_cache: boolean | string no_cache: boolean | string
} }

View File

@@ -47,6 +47,8 @@ import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg'
import { GetCookies } from './user/GetCookie' import { GetCookies } from './user/GetCookie'
import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike' import { SetMsgEmojiLike } from './msg/SetMsgEmojiLike'
import { ForwardFriendSingleMsg, ForwardSingleGroupMsg } from './msg/ForwardSingleMsg' import { ForwardFriendSingleMsg, ForwardSingleGroupMsg } from './msg/ForwardSingleMsg'
import { GetGroupEssence } from './group/GetGroupEssence'
import { GetGroupHonorInfo } from './group/GetGroupHonorInfo'
export const actionHandlers = [ export const actionHandlers = [
new GetFile(), new GetFile(),
@@ -89,6 +91,8 @@ export const actionHandlers = [
new ForwardFriendSingleMsg(), new ForwardFriendSingleMsg(),
new ForwardSingleGroupMsg(), new ForwardSingleGroupMsg(),
//以下为go-cqhttp api //以下为go-cqhttp api
new GetGroupEssence(),
new GetGroupHonorInfo(),
new GoCQHTTPSendForwardMsg(), new GoCQHTTPSendForwardMsg(),
new GoCQHTTPSendGroupForwardMsg(), new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(), new GoCQHTTPSendPrivateForwardMsg(),

View File

@@ -14,11 +14,13 @@ import { friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo } f
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
OB11MessageDataType, OB11MessageFile, OB11MessageDataType,
OB11MessageFile,
OB11MessageJson, OB11MessageJson,
OB11MessageMixType, OB11MessageMixType,
OB11MessageMusic, OB11MessageMusic,
OB11MessageNode, OB11MessageVideo, OB11MessageNode,
OB11MessageVideo,
OB11PostSendMsg, OB11PostSendMsg,
} from '../../types' } from '../../types'
import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg' import { NTQQMsgApi, Peer } from '../../../ntqqapi/api/msg'
@@ -32,7 +34,7 @@ import { ALLOW_SEND_TEMP_MSG, 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'
import { crychic } from '../../../ntqqapi/external/crychic' import { crychic } from '../../../ntqqapi/native/crychic'
import { NTQQGroupApi } from '../../../ntqqapi/api' import { NTQQGroupApi } from '../../../ntqqapi/api'
import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign' import { CustomMusicSignPostData, IdMusicSignPostData, MusicSign, MusicSignPostData } from '../../../common/utils/sign'
@@ -177,11 +179,18 @@ export async function createSendElements(
} }
} }
break break
case OB11MessageDataType.mface: { case OB11MessageDataType.mface:
sendElements.push( {
SendMsgElementConstructor.mface(sendMsg.data.emoji_package_id, sendMsg.data.emoji_id, sendMsg.data.key, sendMsg.data.summary), sendElements.push(
) SendMsgElementConstructor.mface(
}break; sendMsg.data.emoji_package_id,
sendMsg.data.emoji_id,
sendMsg.data.key,
sendMsg.data.summary,
),
)
}
break
case OB11MessageDataType.image: case OB11MessageDataType.image:
case OB11MessageDataType.file: case OB11MessageDataType.file:
case OB11MessageDataType.video: case OB11MessageDataType.video:
@@ -303,14 +312,14 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = convertMessage2List(payload.message) const messages = convertMessage2List(payload.message)
const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node) const fmNum = this.getSpecialMsgNum(messages, OB11MessageDataType.node)
if (fmNum && fmNum != messages.length) { if (fmNum && fmNum != messages.length) {
return { return {
valid: false, valid: false,
message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素', message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素',
} }
} }
const musicNum = this.getSpecialMsgNum(payload, OB11MessageDataType.music) const musicNum = this.getSpecialMsgNum(messages, OB11MessageDataType.music)
if (musicNum && messages.length > 1) { if (musicNum && messages.length > 1) {
return { return {
valid: false, valid: false,
@@ -358,7 +367,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (friend) { if (friend) {
// peer.name = friend.nickName // peer.name = friend.nickName
peer.peerUid = friend.uid peer.peerUid = friend.uid
} else { }
else {
peer.chatType = ChatType.temp peer.chatType = ChatType.temp
const tempUserUid = getUidByUin(payload.user_id.toString()) const tempUserUid = getUidByUin(payload.user_id.toString())
if (!tempUserUid) { if (!tempUserUid) {
@@ -371,25 +381,28 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
if (payload?.group_id && payload.message_type === 'group') { if (payload?.group_id && payload.message_type === 'group') {
await genGroupPeer() await genGroupPeer()
} else if (payload?.user_id) { }
else if (payload?.user_id) {
genFriendPeer() genFriendPeer()
} else if (payload.group_id) { }
else if (payload.group_id) {
await genGroupPeer() await genGroupPeer()
} else { }
else {
throw '发送消息参数错误, 请指定group_id或user_id' throw '发送消息参数错误, 请指定group_id或user_id'
} }
const messages = convertMessage2List( const messages = convertMessage2List(
payload.message, payload.message,
payload.auto_escape === true || payload.auto_escape === 'true', payload.auto_escape === true || payload.auto_escape === 'true',
) )
if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) { if (this.getSpecialMsgNum(messages, OB11MessageDataType.node)) {
try { try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group) const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return { message_id: returnMsg.msgShortId } return { message_id: returnMsg.msgShortId }
} catch (e) { } catch (e) {
throw '发送转发消息失败 ' + e.toString() throw '发送转发消息失败 ' + e.toString()
} }
} else if (this.getSpecialMsgNum(payload, OB11MessageDataType.music)) { } else if (this.getSpecialMsgNum(messages, OB11MessageDataType.music)) {
const music = messages[0] as OB11MessageMusic const music = messages[0] as OB11MessageMusic
if (music) { if (music) {
const { musicSignUrl } = getConfigUtil().getConfig() const { musicSignUrl } = getConfigUtil().getConfig()
@@ -402,24 +415,23 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
const postData: MusicSignPostData = { ...music.data } const postData: MusicSignPostData = { ...music.data }
if (type === 'custom' && music.data.content) { if (type === 'custom' && music.data.content) {
;(postData as CustomMusicSignPostData).singer = music.data.content ;(postData as CustomMusicSignPostData).singer = music.data.content
delete (postData as OB11MessageCustomMusic['data']).content delete (postData as OB11MessageCustomMusic['data']).content
} }
if (type === 'custom'){ if (type === 'custom') {
const customMusicData = music.data as CustomMusicSignPostData const customMusicData = music.data as CustomMusicSignPostData
if (!customMusicData.url){ if (!customMusicData.url) {
throw ('自定义音卡缺少参数url'); throw '自定义音卡缺少参数url'
} }
if (!customMusicData.audio){ if (!customMusicData.audio) {
throw('自定义音卡缺少参数audio'); throw '自定义音卡缺少参数audio'
} }
if (!customMusicData.title){ if (!customMusicData.title) {
throw('自定义音卡缺少参数title'); throw '自定义音卡缺少参数title'
} }
} }
if (type === 'qq' || type === '163') { if (type === 'qq' || type === '163') {
const idMusicData = music.data as IdMusicSignPostData; const idMusicData = music.data as IdMusicSignPostData
if (!idMusicData.id) { if (!idMusicData.id) {
throw '音乐卡片缺少id参数' throw '音乐卡片缺少id参数'
} }
@@ -427,6 +439,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let jsonContent: string let jsonContent: string
try { try {
jsonContent = await new MusicSign(musicSignUrl).sign(postData) jsonContent = await new MusicSign(musicSignUrl).sign(postData)
if (!jsonContent){
throw '音乐消息生成失败,提交内容有误或者签名服务器签名失败'
}
} catch (e) { } catch (e) {
throw `签名音乐消息失败:${e}` throw `签名音乐消息失败:${e}`
} }
@@ -448,9 +463,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
return { message_id: returnMsg.msgShortId } return { message_id: returnMsg.msgShortId }
} }
private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number { private getSpecialMsgNum(message: OB11MessageData[], msgType: OB11MessageDataType): number {
if (Array.isArray(payload.message)) { if (Array.isArray(message)) {
return payload.message.filter((msg) => msg.type == msgType).length return message.filter((msg) => msg.type == msgType).length
} }
return 0 return 0
} }
@@ -502,7 +517,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId)) let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId))
if (!needClone) { if (!needClone) {
nodeMsgIds.push(nodeMsg.msgId) nodeMsgIds.push(nodeMsg.msgId)
} else { }
else {
if (nodeMsg.peerUid !== selfInfo.uid) { if (nodeMsg.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg) const cloneMsg = await this.cloneMsg(nodeMsg)
if (cloneMsg) { if (cloneMsg) {
@@ -510,7 +526,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
} }
} else { }
else {
// 自定义的消息 // 自定义的消息
// 提取消息段发给自己生成消息id // 提取消息段发给自己生成消息id
try { try {
@@ -532,7 +549,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
sendElementsSplit[splitIndex] = [ele] sendElementsSplit[splitIndex] = [ele]
splitIndex++ splitIndex++
} else { }
else {
sendElementsSplit[splitIndex].push(ele) sendElementsSplit[splitIndex].push(ele)
} }
log(sendElementsSplit) log(sendElementsSplit)
@@ -561,7 +579,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
nodeMsgArray.push(nodeMsg) nodeMsgArray.push(nodeMsg)
if (!srcPeer) { if (!srcPeer) {
srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid } srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
} else if (srcPeer.peerUid !== nodeMsg.peerUid) { }
else if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true needSendSelf = true
srcPeer = selfPeer srcPeer = selfPeer
} }

View File

@@ -66,4 +66,6 @@ export enum ActionName {
GoCQHTTP_DownloadFile = 'download_file', GoCQHTTP_DownloadFile = 'download_file',
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history', GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
GoCQHTTP_GetForwardMsg = 'get_forward_msg', GoCQHTTP_GetForwardMsg = 'get_forward_msg',
GoCQHTTP_GetEssenceMsg = "get_essence_msg_list",
GetGroupHonorInfo = "get_group_honor_info",
} }

View File

@@ -3,10 +3,15 @@ import { NTQQUserApi } from '../../../ntqqapi/api'
import { groups } from '../../../common/data' import { groups } from '../../../common/data'
import { ActionName } from '../types' import { ActionName } from '../types'
export class GetCookies extends BaseAction<null, { cookies: string; bkn: string }> { interface Payload {
domain: string
}
export class GetCookies extends BaseAction<Payload, { cookies: string; bkn: string }> {
actionName = ActionName.GetCookies actionName = ActionName.GetCookies
protected async _handle() { protected async _handle(payload: Payload) {
return NTQQUserApi.getCookie(groups[0]) const domain = payload.domain || 'qun.qq.com'
return NTQQUserApi.getCookies(domain);
} }
} }

View File

@@ -6,7 +6,7 @@ import { ActionName } from '../types'
import { NTQQFriendApi } from '../../../ntqqapi/api' import { NTQQFriendApi } from '../../../ntqqapi/api'
import { log } from '../../../common/utils' import { log } from '../../../common/utils'
interface Payload{ interface Payload {
no_cache: boolean | string no_cache: boolean | string
} }

View File

@@ -1,4 +1,4 @@
import fastXmlParser, { XMLParser } from 'fast-xml-parser' import fastXmlParser from 'fast-xml-parser'
import { import {
OB11Group, OB11Group,
OB11GroupMember, OB11GroupMember,
@@ -16,8 +16,7 @@ import {
GrayTipElementSubType, GrayTipElementSubType,
Group, Group,
GroupMember, GroupMember,
IMAGE_HTTP_HOST, PicType,
IMAGE_HTTP_HOST_NT,
RawMessage, RawMessage,
SelfInfo, SelfInfo,
Sex, Sex,
@@ -25,7 +24,7 @@ import {
User, User,
VideoElement, VideoElement,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import { deleteGroup, getFriend, getGroupMember, groups, selfInfo, tempGroupCodeMap } from '../common/data' import { deleteGroup, getFriend, getGroupMember, selfInfo, tempGroupCodeMap } 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'
@@ -46,6 +45,8 @@ import { NTQQGroupApi } from '../ntqqapi/api'
import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent' import { OB11GroupMsgEmojiLikeEvent } from './event/notice/OB11MsgEmojiLikeEvent'
import { mFaceCache } from '../ntqqapi/constructor' import { mFaceCache } from '../ntqqapi/constructor'
import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent' import { OB11FriendAddNoticeEvent } from './event/notice/OB11FriendAddNoticeEvent'
import { OB11FriendRecallNoticeEvent } from './event/notice/OB11FriendRecallNoticeEvent'
import { OB11GroupRecallNoticeEvent } from './event/notice/OB11GroupRecallNoticeEvent'
let lastRKeyUpdateTime = 0 let lastRKeyUpdateTime = 0
@@ -63,6 +64,7 @@ export class OB11Constructor {
time: parseInt(msg.msgTime) || Date.now(), time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId, message_id: msg.msgShortId,
real_id: msg.msgShortId, real_id: 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),
@@ -84,13 +86,15 @@ export class OB11Constructor {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role) resMsg.sender.role = OB11Constructor.groupMemberRole(member.role)
resMsg.sender.nickname = member.nick resMsg.sender.nickname = member.nick
} }
} else if (msg.chatType == ChatType.friend) { }
else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = 'friend' resMsg.sub_type = 'friend'
const friend = await getFriend(msg.senderUin) const friend = await getFriend(msg.senderUin)
if (friend) { if (friend) {
resMsg.sender.nickname = friend.nick resMsg.sender.nickname = friend.nick
} }
} else if (msg.chatType == ChatType.temp) { }
else if (msg.chatType == ChatType.temp) {
resMsg.sub_type = 'group' resMsg.sub_type = 'group'
const tempGroupCode = tempGroupCodeMap[msg.peerUin] const tempGroupCode = tempGroupCodeMap[msg.peerUin]
if (tempGroupCode) { if (tempGroupCode) {
@@ -108,7 +112,8 @@ export class OB11Constructor {
if (element.textElement.atType == AtType.atAll) { if (element.textElement.atType == AtType.atAll) {
// message_data["data"]["mention"] = "all" // message_data["data"]["mention"] = "all"
message_data['data']['qq'] = 'all' message_data['data']['qq'] = 'all'
} else { }
else {
let atUid = element.textElement.atNtUid let atUid = element.textElement.atNtUid
let atQQ = element.textElement.atUid let atQQ = element.textElement.atUid
if (!atQQ || atQQ === '0') { if (!atQQ || atQQ === '0') {
@@ -122,14 +127,16 @@ export class OB11Constructor {
message_data['data']['qq'] = atQQ message_data['data']['qq'] = atQQ
} }
} }
} else if (element.textElement) { }
else if (element.textElement) {
message_data['type'] = 'text' message_data['type'] = 'text'
let text = element.textElement.content let text = element.textElement.content
if (!text.trim()) { if (!text.trim()) {
continue continue
} }
message_data['data']['text'] = text message_data['data']['text'] = text
} else if (element.replyElement) { }
else if (element.replyElement) {
message_data['type'] = 'reply' message_data['type'] = 'reply'
// log("收到回复消息", element.replyElement.replayMsgSeq) // log("收到回复消息", element.replyElement.replayMsgSeq)
try { try {
@@ -137,26 +144,33 @@ export class OB11Constructor {
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId) // log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
if (replyMsg) { if (replyMsg) {
message_data['data']['id'] = replyMsg.msgShortId.toString() message_data['data']['id'] = replyMsg.msgShortId.toString()
} else { }
else {
continue continue
} }
} catch (e) { } catch (e) {
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq) log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq)
} }
} else if (element.picElement) { }
else if (element.picElement) {
message_data['type'] = 'image' message_data['type'] = 'image'
// message_data["data"]["file"] = element.picElement.sourcePath // message_data["data"]["file"] = element.picElement.sourcePath
message_data['data']['file'] = element.picElement.fileName let fileName = element.picElement.fileName;
const sourcePath = element.picElement.sourcePath;
if (element.picElement.picType === PicType.gif && !fileName.endsWith('.gif')){
fileName += ".gif";
}
message_data['data']['file'] = fileName
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
message_data['data']['url'] = await NTQQFileApi.getImageUrl(msg) message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement, msg.chatType);
// message_data["data"]["file_id"] = element.picElement.fileUuid // message_data["data"]["file_id"] = element.picElement.fileUuid
message_data['data']['file_size'] = element.picElement.fileSize message_data['data']['file_size'] = element.picElement.fileSize
dbUtil dbUtil
.addFileCache(element.picElement.fileName, { .addFileCache(fileName, {
fileName: element.picElement.fileName, fileName,
filePath: element.picElement.sourcePath, filePath: sourcePath,
fileSize: element.picElement.fileSize.toString(), fileSize: element.picElement.fileSize.toString(),
url: message_data['data']['url'], url: message_data['data']['url'],
downloadFunc: async () => { downloadFunc: async () => {
@@ -172,7 +186,8 @@ export class OB11Constructor {
}) })
.then() .then()
// 不在自动下载图片 // 不在自动下载图片
} else if (element.videoElement || element.fileElement) { }
else if (element.videoElement || element.fileElement) {
const videoOrFileElement = element.videoElement || element.fileElement const videoOrFileElement = element.videoElement || element.fileElement
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file
message_data['type'] = ob11MessageDataType message_data['type'] = ob11MessageDataType
@@ -201,7 +216,8 @@ export class OB11Constructor {
}) })
.then() .then()
// 怎么拿到url呢 // 怎么拿到url呢
} else if (element.pttElement) { }
else if (element.pttElement) {
message_data['type'] = OB11MessageDataType.voice message_data['type'] = OB11MessageDataType.voice
message_data['data']['file'] = element.pttElement.fileName message_data['data']['file'] = element.pttElement.fileName
message_data['data']['path'] = element.pttElement.filePath message_data['data']['path'] = element.pttElement.filePath
@@ -221,22 +237,27 @@ export class OB11Constructor {
// }).catch(err => { // }).catch(err => {
// console.log("语音转文字失败", err); // console.log("语音转文字失败", err);
// }) // })
} else if (element.arkElement) { }
else if (element.arkElement) {
message_data['type'] = OB11MessageDataType.json message_data['type'] = OB11MessageDataType.json
message_data['data']['data'] = element.arkElement.bytesData message_data['data']['data'] = element.arkElement.bytesData
} else if (element.faceElement) { }
else if (element.faceElement) {
const faceId = element.faceElement.faceIndex const faceId = element.faceElement.faceIndex
if (faceId === FaceIndex.dice) { if (faceId === FaceIndex.dice) {
message_data['type'] = OB11MessageDataType.dice message_data['type'] = OB11MessageDataType.dice
message_data['data']['result'] = element.faceElement.resultId message_data['data']['result'] = element.faceElement.resultId
} else if (faceId === FaceIndex.RPS) { }
else if (faceId === FaceIndex.RPS) {
message_data['type'] = OB11MessageDataType.RPS message_data['type'] = OB11MessageDataType.RPS
message_data['data']['result'] = element.faceElement.resultId message_data['data']['result'] = element.faceElement.resultId
} else { }
else {
message_data['type'] = OB11MessageDataType.face message_data['type'] = OB11MessageDataType.face
message_data['data']['id'] = element.faceElement.faceIndex.toString() message_data['data']['id'] = element.faceElement.faceIndex.toString()
} }
} else if (element.marketFaceElement) { }
else if (element.marketFaceElement) {
message_data['type'] = OB11MessageDataType.mface message_data['type'] = OB11MessageDataType.mface
message_data['data']['summary'] = element.marketFaceElement.faceName message_data['data']['summary'] = element.marketFaceElement.faceName
const md5 = element.marketFaceElement.emojiId const md5 = element.marketFaceElement.emojiId
@@ -249,11 +270,13 @@ export class OB11Constructor {
message_data['data']['emoji_id'] = element.marketFaceElement.emojiId message_data['data']['emoji_id'] = element.marketFaceElement.emojiId
message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId) message_data['data']['emoji_package_id'] = String(element.marketFaceElement.emojiPackageId)
message_data['data']['key'] = element.marketFaceElement.key message_data['data']['key'] = element.marketFaceElement.key
mFaceCache.set(md5, element.marketFaceElement.faceName); mFaceCache.set(md5, element.marketFaceElement.faceName)
} else if (element.markdownElement) { }
else if (element.markdownElement) {
message_data['type'] = OB11MessageDataType.markdown message_data['type'] = OB11MessageDataType.markdown
message_data['data']['data'] = element.markdownElement.content message_data['data']['data'] = element.markdownElement.content
} else if (element.multiForwardMsgElement) { }
else if (element.multiForwardMsgElement) {
message_data['type'] = OB11MessageDataType.forward message_data['type'] = OB11MessageDataType.forward
message_data['data']['id'] = msg.msgId message_data['data']['id'] = msg.msgId
} }
@@ -261,7 +284,8 @@ export class OB11Constructor {
const cqCode = encodeCQCode(message_data) const cqCode = encodeCQCode(message_data)
if (messagePostFormat === 'string') { if (messagePostFormat === 'string') {
;(resMsg.message as string) += cqCode ;(resMsg.message as string) += cqCode
} else (resMsg.message as OB11MessageData[]).push(message_data) }
else (resMsg.message as OB11MessageData[]).push(message_data)
resMsg.raw_message += cqCode resMsg.raw_message += cqCode
} }
@@ -310,7 +334,8 @@ export class OB11Constructor {
// log("构造群增加事件", event) // log("构造群增加事件", event)
return event return event
} }
} else if (groupElement.type === TipGroupElementType.ban) { }
else if (groupElement.type === TipGroupElementType.ban) {
log('收到群群员禁言提示', groupElement) log('收到群群员禁言提示', groupElement)
const memberUid = groupElement.shutUp.member.uid const memberUid = groupElement.shutUp.member.uid
const adminUid = groupElement.shutUp.admin.uid const adminUid = groupElement.shutUp.admin.uid
@@ -321,7 +346,8 @@ export class OB11Constructor {
memberUin = memberUin =
(await getGroupMember(msg.peerUid, memberUid))?.uin || (await getGroupMember(msg.peerUid, memberUid))?.uin ||
(await NTQQUserApi.getUserDetailInfo(memberUid))?.uin (await NTQQUserApi.getUserDetailInfo(memberUid))?.uin
} else { }
else {
memberUin = '0' // 0表示全员禁言 memberUin = '0' // 0表示全员禁言
if (duration > 0) { if (duration > 0) {
duration = -1 duration = -1
@@ -338,7 +364,8 @@ export class OB11Constructor {
sub_type, sub_type,
) )
} }
} else if (groupElement.type == TipGroupElementType.kicked) { }
else if (groupElement.type == TipGroupElementType.kicked) {
log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement) log(`收到我被踢出或退群提示, 群${msg.peerUid}`, groupElement)
deleteGroup(msg.peerUid) deleteGroup(msg.peerUid)
NTQQGroupApi.quitGroup(msg.peerUid).then() NTQQGroupApi.quitGroup(msg.peerUid).then()
@@ -358,7 +385,8 @@ export class OB11Constructor {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave') return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), 0, 'leave')
} }
} }
} else if (element.fileElement) { }
else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), { return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {
id: element.fileElement.fileUuid, id: element.fileElement.fileUuid,
name: element.fileElement.fileName, name: element.fileElement.fileName,
@@ -423,7 +451,8 @@ export class OB11Constructor {
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite') return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite')
} }
} }
} else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) { }
else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr) const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr)
/* /*
{ {
@@ -463,14 +492,41 @@ export class OB11Constructor {
static async FriendAddEvent(msg: RawMessage): Promise<OB11FriendAddNoticeEvent | undefined> { static async FriendAddEvent(msg: RawMessage): Promise<OB11FriendAddNoticeEvent | undefined> {
if (msg.chatType !== ChatType.friend) { if (msg.chatType !== ChatType.friend) {
return; return
} }
if (msg.msgType === 5 && msg.subMsgType === 12) { if (msg.msgType === 5 && msg.subMsgType === 12) {
const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin)); const event = new OB11FriendAddNoticeEvent(parseInt(msg.peerUin))
return event; return event
} }
return; return
} }
static async RecallEvent(
msg: RawMessage,
): Promise<OB11FriendRecallNoticeEvent | OB11GroupRecallNoticeEvent | undefined> {
let msgElement = msg.elements.find(
(element) => element.grayTipElement?.subElementType === GrayTipElementSubType.RECALL,
)
if (!msgElement) {
return
}
const isGroup = msg.chatType === ChatType.group
const revokeElement = msgElement.grayTipElement.revokeElement
if (isGroup) {
const operator = await getGroupMember(msg.peerUid, revokeElement.operatorUid)
const sender = await getGroupMember(msg.peerUid, revokeElement.origMsgSenderUid)
return new OB11GroupRecallNoticeEvent(
parseInt(msg.peerUid),
parseInt(sender.uin),
parseInt(operator.uin),
msg.msgShortId,
)
}
else {
return new OB11FriendRecallNoticeEvent(parseInt(msg.senderUin), msg.msgShortId)
}
}
static friend(friend: User): OB11User { static friend(friend: User): OB11User {
return { return {
user_id: parseInt(friend.uin), user_id: parseInt(friend.uin),

View File

@@ -1,11 +1,11 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'
export class OB11FriendAddNoticeEvent extends OB11BaseNoticeEvent { export class OB11FriendAddNoticeEvent extends OB11BaseNoticeEvent {
notice_type = 'friend_add'; notice_type = 'friend_add'
user_id: number; user_id: number
public constructor(userId: number) { public constructor(userId: number) {
super(); super()
this.user_id = userId; this.user_id = userId
} }
} }

View File

@@ -1,14 +1,14 @@
import { Response } from 'express' import { Response } from 'express'
import { OB11Response } from '../action/OB11Response' import { OB11Response } from '../action/OB11Response'
import { HttpServerBase } from '../../common/server/http' import { HttpServerBase } from '@/common/server/http'
import { actionHandlers, actionMap } from '../action' import { actionHandlers, actionMap } from '../action'
import { getConfigUtil } from '../../common/config' import { getConfigUtil } from '@/common/config'
import { postOB11Event } from './postOB11Event' import { postOB11Event } from './postOB11Event'
import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent' import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'
import { selfInfo } from '../../common/data' import { selfInfo } from '@/common/data'
class OB11HTTPServer extends HttpServerBase { class OB11HTTPServer extends HttpServerBase {
name = 'OneBot V11 server' name = 'LLOneBot server'
handleFailed(res: Response, payload: any, e: any) { handleFailed(res: Response, payload: any, e: any) {
res.send(OB11Response.error(e.stack.toString(), 200)) res.send(OB11Response.error(e.stack.toString(), 200))

View File

@@ -75,6 +75,7 @@ export interface OB11Message {
self_id?: number self_id?: number
time: number time: number
message_id: number message_id: number
message_seq: number // go-cqhttp字段实际上是message_id
real_id: number real_id: number
user_id: number user_id: number
group_id?: number group_id?: number

View File

@@ -1 +1 @@
export const version = '3.24.0' export const version = '3.24.4'

View File

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

View File

@@ -7,8 +7,21 @@
"esModuleInterop": true, "esModuleInterop": true,
"allowJs": true, "allowJs": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"moduleResolution": "node" "experimentalDecorators": true,
// "sourceMap": true "resolveJsonModule": true,
"moduleResolution": "node",
"paths": {
"@/common/*": [
"./src/common/*"
],
"@/onebot11/*": [
"./src/onebot11"
],
"@/ntqqapi/*": [
"./src/ntqqapi/*"
]
},
"noEmit": true
}, },
"include": ["src/*", "src/**/*", "scripts/*"], "include": ["src/*", "src/**/*", "scripts/*"],
"exclude": ["node_modules"] "exclude": ["node_modules"]