mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-06 13:05:09 +00:00
NapCatQQ
This commit is contained in:
49
src/onebot11/action/BaseAction.ts
Normal file
49
src/onebot11/action/BaseAction.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ActionName, BaseCheckResult } from './types';
|
||||
import { OB11Response } from './OB11Response';
|
||||
import { OB11Return } from '../types';
|
||||
|
||||
import { log } from '../../common/utils/log';
|
||||
|
||||
class BaseAction<PayloadType, ReturnDataType> {
|
||||
actionName: ActionName;
|
||||
|
||||
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
|
||||
return {
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
|
||||
const result = await this.check(payload);
|
||||
if (!result.valid) {
|
||||
return OB11Response.error(result.message, 400);
|
||||
}
|
||||
try {
|
||||
const resData = await this._handle(payload);
|
||||
return OB11Response.ok(resData);
|
||||
} catch (e) {
|
||||
log('发生错误', e);
|
||||
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200);
|
||||
}
|
||||
}
|
||||
|
||||
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
|
||||
const result = await this.check(payload);
|
||||
if (!result.valid) {
|
||||
return OB11Response.error(result.message, 1400);
|
||||
}
|
||||
try {
|
||||
const resData = await this._handle(payload);
|
||||
return OB11Response.ok(resData, echo);
|
||||
} catch (e) {
|
||||
log('发生错误', e);
|
||||
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo);
|
||||
}
|
||||
}
|
||||
|
||||
protected async _handle(payload: PayloadType): Promise<ReturnDataType> {
|
||||
throw `pleas override ${this.actionName} _handle`;
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseAction;
|
||||
32
src/onebot11/action/OB11Response.ts
Normal file
32
src/onebot11/action/OB11Response.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { OB11Return } from '../types';
|
||||
|
||||
import { isNull } from '../../common/utils/helper';
|
||||
|
||||
export class OB11Response {
|
||||
static res<T>(data: T, status: string, retcode: number, message: string = ''): OB11Return<T> {
|
||||
return {
|
||||
status: status,
|
||||
retcode: retcode,
|
||||
data: data,
|
||||
message: message,
|
||||
wording: message,
|
||||
echo: null
|
||||
};
|
||||
}
|
||||
|
||||
static ok<T>(data: T, echo: any = null) {
|
||||
const res = OB11Response.res<T>(data, 'ok', 0);
|
||||
if (!isNull(echo)) {
|
||||
res.echo = echo;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static error(err: string, retcode: number, echo: any = null) {
|
||||
const res = OB11Response.res(null, 'failed', retcode, err);
|
||||
if (!isNull(echo)) {
|
||||
res.echo = echo;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
116
src/onebot11/action/file/GetFile.ts
Normal file
116
src/onebot11/action/file/GetFile.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import fs from 'fs/promises';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
import { log } from '@/common/utils/log';
|
||||
import { sleep } from '@/common/utils/helper';
|
||||
import { uri2local } from '@/common/utils/file';
|
||||
import { ActionName } from '../types';
|
||||
import { FileElement, RawMessage, VideoElement } from '@/core/qqnt/entities';
|
||||
import { NTQQFileApi } from '@/core/qqnt/apis';
|
||||
|
||||
export interface GetFilePayload {
|
||||
file: string; // 文件名或者fileUuid
|
||||
}
|
||||
|
||||
export interface GetFileResponse {
|
||||
file?: string; // path
|
||||
url?: string;
|
||||
file_size?: string;
|
||||
file_name?: string;
|
||||
base64?: string;
|
||||
}
|
||||
|
||||
|
||||
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
|
||||
private getElement(msg: RawMessage): { id: string, element: VideoElement | FileElement } {
|
||||
let element = msg.elements.find(e => e.fileElement);
|
||||
if (!element) {
|
||||
element = msg.elements.find(e => e.videoElement);
|
||||
if (element) {
|
||||
return { id: element.elementId, element: element.videoElement };
|
||||
} else {
|
||||
throw new Error('找不到文件');
|
||||
}
|
||||
}
|
||||
return { id: element.elementId, element: element.fileElement };
|
||||
}
|
||||
|
||||
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
|
||||
let cache = await dbUtil.getFileCacheByName(payload.file);
|
||||
if (!cache) {
|
||||
cache = await dbUtil.getFileCacheByUuid(payload.file);
|
||||
}
|
||||
if (!cache) {
|
||||
throw new Error('file not found');
|
||||
}
|
||||
const { enableLocalFile2Url } = ob11Config;
|
||||
try {
|
||||
await fs.access(cache.path, fs.constants.F_OK);
|
||||
} catch (e) {
|
||||
log('local file not found, start download...');
|
||||
// if (cache.url) {
|
||||
// const downloadResult = await uri2local(cache.url);
|
||||
// if (downloadResult.success) {
|
||||
// cache.path = downloadResult.path;
|
||||
// dbUtil.updateFileCache(cache).then();
|
||||
// } else {
|
||||
// throw new Error('file download failed. ' + downloadResult.errMsg);
|
||||
// }
|
||||
// } else {
|
||||
// // 没有url的可能是私聊文件或者群文件,需要自己下载
|
||||
// log('需要调用 NTQQ 下载文件api');
|
||||
let msg = await dbUtil.getMsgByLongId(cache.msgId);
|
||||
// log('文件 msg', msg);
|
||||
if (msg) {
|
||||
// 构建下载函数
|
||||
const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
|
||||
cache.elementId, '', '');
|
||||
// await sleep(1000);
|
||||
|
||||
// log('download result', downloadPath);
|
||||
msg = await dbUtil.getMsgByLongId(cache.msgId);
|
||||
// log('下载完成后的msg', msg);
|
||||
cache.path = downloadPath!;
|
||||
dbUtil.updateFileCache(cache).then();
|
||||
// log('下载完成后的msg', msg);
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
// log('file found', cache);
|
||||
const res: GetFileResponse = {
|
||||
file: cache.path,
|
||||
url: cache.url,
|
||||
file_size: cache.size.toString(),
|
||||
file_name: cache.name
|
||||
};
|
||||
if (enableLocalFile2Url) {
|
||||
if (!cache.url) {
|
||||
try {
|
||||
res.base64 = await fs.readFile(cache.path, 'base64');
|
||||
} catch (e) {
|
||||
throw new Error('文件下载失败. ' + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (autoDeleteFile) {
|
||||
// setTimeout(() => {
|
||||
// fs.unlink(cache.filePath)
|
||||
// }, autoDeleteFileSecond * 1000)
|
||||
// }
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export default class GetFile extends GetFileBase {
|
||||
actionName = ActionName.GetFile;
|
||||
|
||||
protected async _handle(payload: { file_id: string, file: string }): Promise<GetFileResponse> {
|
||||
if (!payload.file_id) {
|
||||
throw new Error('file_id 不能为空');
|
||||
}
|
||||
payload.file = payload.file_id;
|
||||
return super._handle(payload);
|
||||
}
|
||||
}
|
||||
7
src/onebot11/action/file/GetImage.ts
Normal file
7
src/onebot11/action/file/GetImage.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { GetFileBase } from './GetFile';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
|
||||
export default class GetImage extends GetFileBase {
|
||||
actionName = ActionName.GetImage;
|
||||
}
|
||||
15
src/onebot11/action/file/GetRecord.ts
Normal file
15
src/onebot11/action/file/GetRecord.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
interface Payload extends GetFilePayload {
|
||||
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
|
||||
}
|
||||
|
||||
export default class GetRecord extends GetFileBase {
|
||||
actionName = ActionName.GetRecord;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<GetFileResponse> {
|
||||
const res = super._handle(payload);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
73
src/onebot11/action/go-cqhttp/DownloadFile.ts
Normal file
73
src/onebot11/action/go-cqhttp/DownloadFile.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import fs from 'fs';
|
||||
import { join as joinPath } from 'node:path';
|
||||
import { calculateFileMD5, getTempDir, httpDownload } from '@/common/utils/file';
|
||||
import { v4 as uuid4 } from 'uuid';
|
||||
|
||||
interface Payload {
|
||||
thread_count?: number;
|
||||
url?: string;
|
||||
base64?: string;
|
||||
name?: string;
|
||||
headers?: string | string[];
|
||||
}
|
||||
|
||||
interface FileResponse {
|
||||
file: string;
|
||||
}
|
||||
|
||||
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
|
||||
actionName = ActionName.GoCQHTTP_DownloadFile;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<FileResponse> {
|
||||
const isRandomName = !payload.name;
|
||||
const name = payload.name || uuid4();
|
||||
const filePath = joinPath(getTempDir(), name);
|
||||
|
||||
if (payload.base64) {
|
||||
fs.writeFileSync(filePath, payload.base64, 'base64');
|
||||
} else if (payload.url) {
|
||||
const headers = this.getHeaders(payload.headers);
|
||||
const buffer = await httpDownload({ url: payload.url, headers: headers });
|
||||
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary');
|
||||
} else {
|
||||
throw new Error('不存在任何文件, 无法下载');
|
||||
}
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
||||
if (isRandomName) {
|
||||
// 默认实现要名称未填写时文件名为文件 md5
|
||||
const md5 = await calculateFileMD5(filePath);
|
||||
const newPath = joinPath(getTempDir(), md5);
|
||||
fs.renameSync(filePath, newPath);
|
||||
return { file: newPath };
|
||||
}
|
||||
return { file: filePath };
|
||||
} else {
|
||||
throw new Error('文件写入失败, 检查权限');
|
||||
}
|
||||
}
|
||||
|
||||
getHeaders(headersIn?: string | string[]): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (typeof headersIn == 'string') {
|
||||
headersIn = headersIn.split('[\\r\\n]');
|
||||
}
|
||||
if (Array.isArray(headersIn)) {
|
||||
for (const headerItem of headersIn) {
|
||||
const spilt = headerItem.indexOf('=');
|
||||
if (spilt < 0) {
|
||||
headers[headerItem] = '';
|
||||
} else {
|
||||
const key = headerItem.substring(0, spilt);
|
||||
headers[key] = headerItem.substring(0, spilt + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
43
src/onebot11/action/go-cqhttp/GetForwardMsg.ts
Normal file
43
src/onebot11/action/go-cqhttp/GetForwardMsg.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types';
|
||||
import { NTQQMsgApi } from '@/core/qqnt/apis';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
interface Payload {
|
||||
message_id: string; // long msg id
|
||||
}
|
||||
|
||||
interface Response {
|
||||
messages: (OB11Message & { content: OB11MessageData })[];
|
||||
}
|
||||
|
||||
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> {
|
||||
actionName = ActionName.GoCQHTTP_GetForwardMsg;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<any> {
|
||||
const rootMsg = await dbUtil.getMsgByLongId(payload.message_id);
|
||||
if (!rootMsg) {
|
||||
throw Error('msg not found');
|
||||
}
|
||||
const data = await NTQQMsgApi.getMultiMsg({
|
||||
chatType: rootMsg.chatType,
|
||||
peerUid: rootMsg.peerUid
|
||||
}, rootMsg.msgId, rootMsg.msgId);
|
||||
if (!data || data.result !== 0) {
|
||||
throw Error('找不到相关的聊天记录' + data?.errMsg);
|
||||
}
|
||||
const msgList = data.msgList;
|
||||
const messages = await Promise.all(msgList.map(async msg => {
|
||||
const resMsg = await OB11Constructor.message(msg);
|
||||
resMsg.message_id = await dbUtil.addMsg(msg);
|
||||
return resMsg;
|
||||
}));
|
||||
messages.map(msg => {
|
||||
(<OB11ForwardMessage>msg).content = msg.message;
|
||||
delete (<any>msg).message;
|
||||
});
|
||||
return {messages};
|
||||
}
|
||||
}
|
||||
43
src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts
Normal file
43
src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { OB11Message, OB11User } from '../../types';
|
||||
import { getGroup, groups } from '@/common/data';
|
||||
import { ActionName } from '../types';
|
||||
import { ChatType } from '@/core/qqnt/entities';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { NTQQMsgApi } from '@/core/qqnt/apis/msg';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
|
||||
|
||||
interface Payload {
|
||||
group_id: number
|
||||
message_seq: number,
|
||||
count: number
|
||||
}
|
||||
|
||||
interface Response {
|
||||
messages: OB11Message[];
|
||||
}
|
||||
|
||||
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
|
||||
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<Response> {
|
||||
const group = await getGroup(payload.group_id.toString());
|
||||
if (!group) {
|
||||
throw `群${payload.group_id}不存在`;
|
||||
}
|
||||
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0';
|
||||
// log("startMsgId", startMsgId)
|
||||
const historyResult = (await NTQQMsgApi.getMsgHistory({
|
||||
chatType: ChatType.group,
|
||||
peerUid: group.groupCode
|
||||
}, startMsgId, parseInt(payload.count?.toString()) || 20));
|
||||
console.log(historyResult);
|
||||
const msgList = historyResult.msgList;
|
||||
await Promise.all(msgList.map(async msg => {
|
||||
msg.id = await dbUtil.addMsg(msg);
|
||||
}));
|
||||
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
|
||||
return { 'messages': ob11MsgList };
|
||||
}
|
||||
}
|
||||
22
src/onebot11/action/go-cqhttp/GetStrangerInfo.ts
Normal file
22
src/onebot11/action/go-cqhttp/GetStrangerInfo.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { OB11User } from '../../types';
|
||||
import { getUidByUin, uid2UinMap } from '@/common/data';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQUserApi } from '@/core/qqnt/apis/user';
|
||||
import { log } from '@/common/utils/log';
|
||||
|
||||
|
||||
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> {
|
||||
actionName = ActionName.GoCQHTTP_GetStrangerInfo;
|
||||
|
||||
protected async _handle(payload: { user_id: number }): Promise<OB11User> {
|
||||
const user_id = payload.user_id.toString();
|
||||
log('uidMaps', uid2UinMap);
|
||||
const uid = getUidByUin(user_id);
|
||||
if (!uid) {
|
||||
throw new Error('查无此人');
|
||||
}
|
||||
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid));
|
||||
}
|
||||
}
|
||||
20
src/onebot11/action/go-cqhttp/SendForwardMsg.ts
Normal file
20
src/onebot11/action/go-cqhttp/SendForwardMsg.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import SendMsg, { convertMessage2List } from '../msg/SendMsg';
|
||||
import { OB11PostSendMsg } from '../../types';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
export class GoCQHTTPSendForwardMsg extends SendMsg {
|
||||
actionName = ActionName.GoCQHTTP_SendForwardMsg;
|
||||
|
||||
protected async check(payload: OB11PostSendMsg) {
|
||||
if (payload.messages) payload.message = convertMessage2List(payload.messages);
|
||||
return super.check(payload);
|
||||
}
|
||||
}
|
||||
|
||||
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg {
|
||||
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
|
||||
}
|
||||
|
||||
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg {
|
||||
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
|
||||
}
|
||||
37
src/onebot11/action/go-cqhttp/UploadGroupFile.ts
Normal file
37
src/onebot11/action/go-cqhttp/UploadGroupFile.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { getGroup } from '@/common/data';
|
||||
import { ActionName } from '../types';
|
||||
import { SendMsgElementConstructor } from '@/core/qqnt/entities/constructor';
|
||||
import { ChatType, SendFileElement } from '@/core/qqnt/entities';
|
||||
import fs from 'fs';
|
||||
import { NTQQMsgApi } from '@/core/qqnt/apis/msg';
|
||||
import { uri2local } from '@/common/utils/file';
|
||||
|
||||
interface Payload {
|
||||
group_id: number;
|
||||
file: string;
|
||||
name: string;
|
||||
folder: string;
|
||||
}
|
||||
|
||||
export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.GoCQHTTP_UploadGroupFile;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const group = await getGroup(payload.group_id.toString());
|
||||
if (!group) {
|
||||
throw new Error(`群组${payload.group_id}不存在`);
|
||||
}
|
||||
let file = payload.file;
|
||||
if (fs.existsSync(file)) {
|
||||
file = `file://${file}`;
|
||||
}
|
||||
const downloadResult = await uri2local(file);
|
||||
if (downloadResult.errMsg) {
|
||||
throw new Error(downloadResult.errMsg);
|
||||
}
|
||||
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
|
||||
await NTQQMsgApi.sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
24
src/onebot11/action/group/GetGroupInfo.ts
Normal file
24
src/onebot11/action/group/GetGroupInfo.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getGroup } from '@/common/data';
|
||||
import { OB11Group } from '../../types';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
interface PayloadType {
|
||||
group_id: number
|
||||
}
|
||||
|
||||
class GetGroupInfo extends BaseAction<PayloadType, OB11Group> {
|
||||
actionName = ActionName.GetGroupInfo;
|
||||
|
||||
protected async _handle(payload: PayloadType) {
|
||||
const group = await getGroup(payload.group_id.toString());
|
||||
if (group) {
|
||||
return OB11Constructor.group(group);
|
||||
} else {
|
||||
throw `群${payload.group_id}不存在`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GetGroupInfo;
|
||||
20
src/onebot11/action/group/GetGroupList.ts
Normal file
20
src/onebot11/action/group/GetGroupList.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { OB11Group } from '../../types';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { groups } from '@/common/data';
|
||||
|
||||
|
||||
class GetGroupList extends BaseAction<null, OB11Group[]> {
|
||||
actionName = ActionName.GetGroupList;
|
||||
|
||||
protected async _handle(payload: null) {
|
||||
// if (groups.length === 0) {
|
||||
// const groups = await NTQQGroupApi.getGroups(true)
|
||||
// log("get groups", groups)
|
||||
// }
|
||||
return OB11Constructor.groups(Array.from(groups.values()));
|
||||
}
|
||||
}
|
||||
|
||||
export default GetGroupList;
|
||||
38
src/onebot11/action/group/GetGroupMemberInfo.ts
Normal file
38
src/onebot11/action/group/GetGroupMemberInfo.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { OB11GroupMember } from '../../types';
|
||||
import { getGroupMember } from '../../../common/data';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQUserApi } from '@/core/qqnt/apis/user';
|
||||
import { log } from '../../../common/utils/log';
|
||||
import { isNull } from '../../../common/utils/helper';
|
||||
|
||||
|
||||
export interface PayloadType {
|
||||
group_id: number;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
|
||||
actionName = ActionName.GetGroupMemberInfo;
|
||||
|
||||
protected async _handle(payload: PayloadType) {
|
||||
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString());
|
||||
// log(member);
|
||||
if (member) {
|
||||
log('获取群成员详细信息');
|
||||
try {
|
||||
const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
|
||||
log('群成员详细信息结果', info);
|
||||
Object.assign(member, info);
|
||||
} catch (e) {
|
||||
log('获取群成员详细信息失败, 只能返回基础信息', e);
|
||||
}
|
||||
return OB11Constructor.groupMember(payload.group_id.toString(), member);
|
||||
} else {
|
||||
throw (`群成员${payload.user_id}不存在`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GetGroupMemberInfo;
|
||||
26
src/onebot11/action/group/GetGroupMemberList.ts
Normal file
26
src/onebot11/action/group/GetGroupMemberList.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getGroup } from '@/common/data';
|
||||
import { OB11GroupMember } from '../../types';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { napCatCore } from '@/core';
|
||||
|
||||
export interface PayloadType {
|
||||
group_id: number
|
||||
}
|
||||
|
||||
|
||||
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> {
|
||||
actionName = ActionName.GetGroupMemberList;
|
||||
|
||||
protected async _handle(payload: PayloadType) {
|
||||
const group = await getGroup(payload.group_id.toString());
|
||||
if (group) {
|
||||
return OB11Constructor.groupMembers(group);
|
||||
} else {
|
||||
throw (`群${payload.group_id}不存在`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GetGroupMemberList;
|
||||
10
src/onebot11/action/group/GetGuildList.ts
Normal file
10
src/onebot11/action/group/GetGuildList.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
export default class GetGuildList extends BaseAction<null, null> {
|
||||
actionName = ActionName.GetGuildList;
|
||||
|
||||
protected async _handle(payload: null): Promise<null> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
16
src/onebot11/action/group/SendGroupMsg.ts
Normal file
16
src/onebot11/action/group/SendGroupMsg.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import SendMsg from '../msg/SendMsg';
|
||||
import { ActionName, BaseCheckResult } from '../types';
|
||||
import { OB11PostSendMsg } from '../../types';
|
||||
|
||||
|
||||
class SendGroupMsg extends SendMsg {
|
||||
actionName = ActionName.SendGroupMsg;
|
||||
|
||||
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||
delete payload.user_id;
|
||||
payload.message_type = 'group';
|
||||
return super.check(payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default SendGroupMsg;
|
||||
31
src/onebot11/action/group/SetGroupAddRequest.ts
Normal file
31
src/onebot11/action/group/SetGroupAddRequest.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { GroupRequestOperateTypes } from '@/core/qqnt/entities';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
import { groupNotifies } from '@/common/data';
|
||||
|
||||
interface Payload {
|
||||
flag: string,
|
||||
// sub_type: "add" | "invite",
|
||||
// type: "add" | "invite"
|
||||
approve: boolean,
|
||||
reason: string
|
||||
}
|
||||
|
||||
export default class SetGroupAddRequest extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetGroupAddRequest;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const flag = payload.flag.toString();
|
||||
const approve = payload.approve.toString() === 'true';
|
||||
const notify = groupNotifies[flag];
|
||||
if (!notify) {
|
||||
throw `${flag}对应的加群通知不存在`;
|
||||
}
|
||||
await NTQQGroupApi.handleGroupRequest(notify,
|
||||
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
|
||||
payload.reason
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
25
src/onebot11/action/group/SetGroupAdmin.ts
Normal file
25
src/onebot11/action/group/SetGroupAdmin.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { getGroupMember } from '@/common/data';
|
||||
import { GroupMemberRole } from '@/core/qqnt/entities';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
|
||||
interface Payload {
|
||||
group_id: number,
|
||||
user_id: number,
|
||||
enable: boolean
|
||||
}
|
||||
|
||||
export default class SetGroupAdmin extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetGroupAdmin;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const member = await getGroupMember(payload.group_id, payload.user_id);
|
||||
const enable = payload.enable.toString() === 'true';
|
||||
if (!member) {
|
||||
throw `群成员${payload.user_id}不存在`;
|
||||
}
|
||||
await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, enable ? GroupMemberRole.admin : GroupMemberRole.normal);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
24
src/onebot11/action/group/SetGroupBan.ts
Normal file
24
src/onebot11/action/group/SetGroupBan.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { getGroupMember } from '../../../common/data';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
|
||||
interface Payload {
|
||||
group_id: number,
|
||||
user_id: number,
|
||||
duration: number
|
||||
}
|
||||
|
||||
export default class SetGroupBan extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetGroupBan;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const member = await getGroupMember(payload.group_id, payload.user_id);
|
||||
if (!member) {
|
||||
throw `群成员${payload.user_id}不存在`;
|
||||
}
|
||||
await NTQQGroupApi.banMember(payload.group_id.toString(),
|
||||
[{ uid: member.uid, timeStamp: parseInt(payload.duration.toString()) }]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
23
src/onebot11/action/group/SetGroupCard.ts
Normal file
23
src/onebot11/action/group/SetGroupCard.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { getGroupMember } from '../../../common/data';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
|
||||
interface Payload {
|
||||
group_id: number,
|
||||
user_id: number,
|
||||
card: string
|
||||
}
|
||||
|
||||
export default class SetGroupCard extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetGroupCard;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const member = await getGroupMember(payload.group_id, payload.user_id);
|
||||
if (!member) {
|
||||
throw `群成员${payload.user_id}不存在`;
|
||||
}
|
||||
await NTQQGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || '');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
23
src/onebot11/action/group/SetGroupKick.ts
Normal file
23
src/onebot11/action/group/SetGroupKick.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { getGroupMember } from '../../../common/data';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
|
||||
interface Payload {
|
||||
group_id: number,
|
||||
user_id: number,
|
||||
reject_add_request: boolean
|
||||
}
|
||||
|
||||
export default class SetGroupKick extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetGroupKick;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const member = await getGroupMember(payload.group_id, payload.user_id);
|
||||
if (!member) {
|
||||
throw `群成员${payload.user_id}不存在`;
|
||||
}
|
||||
await NTQQGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
22
src/onebot11/action/group/SetGroupLeave.ts
Normal file
22
src/onebot11/action/group/SetGroupLeave.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
import { log } from '../../../common/utils/log';
|
||||
|
||||
interface Payload {
|
||||
group_id: number,
|
||||
is_dismiss: boolean
|
||||
}
|
||||
|
||||
export default class SetGroupLeave extends BaseAction<Payload, any> {
|
||||
actionName = ActionName.SetGroupLeave;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<any> {
|
||||
try {
|
||||
await NTQQGroupApi.quitGroup(payload.group_id.toString());
|
||||
} catch (e) {
|
||||
log('退群失败', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/onebot11/action/group/SetGroupName.ts
Normal file
18
src/onebot11/action/group/SetGroupName.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
|
||||
interface Payload {
|
||||
group_id: number,
|
||||
group_name: string
|
||||
}
|
||||
|
||||
export default class SetGroupName extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetGroupName;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
|
||||
await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
18
src/onebot11/action/group/SetGroupWholeBan.ts
Normal file
18
src/onebot11/action/group/SetGroupWholeBan.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
|
||||
interface Payload {
|
||||
group_id: number,
|
||||
enable: boolean
|
||||
}
|
||||
|
||||
export default class SetGroupWholeBan extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetGroupWholeBan;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const enable = payload.enable.toString() === 'true';
|
||||
await NTQQGroupApi.banGroup(payload.group_id.toString(), enable);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
104
src/onebot11/action/index.ts
Normal file
104
src/onebot11/action/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import GetMsg from './msg/GetMsg';
|
||||
import GetLoginInfo from './system/GetLoginInfo';
|
||||
import GetFriendList from './user/GetFriendList';
|
||||
import GetGroupList from './group/GetGroupList';
|
||||
import GetGroupInfo from './group/GetGroupInfo';
|
||||
import GetGroupMemberList from './group/GetGroupMemberList';
|
||||
import GetGroupMemberInfo from './group/GetGroupMemberInfo';
|
||||
import SendGroupMsg from './group/SendGroupMsg';
|
||||
import SendPrivateMsg from './msg/SendPrivateMsg';
|
||||
import SendMsg from './msg/SendMsg';
|
||||
import DeleteMsg from './msg/DeleteMsg';
|
||||
import BaseAction from './BaseAction';
|
||||
import GetVersionInfo from './system/GetVersionInfo';
|
||||
import CanSendRecord from './system/CanSendRecord';
|
||||
import CanSendImage from './system/CanSendImage';
|
||||
import GetStatus from './system/GetStatus';
|
||||
import {
|
||||
GoCQHTTPSendForwardMsg,
|
||||
GoCQHTTPSendGroupForwardMsg,
|
||||
GoCQHTTPSendPrivateForwardMsg
|
||||
} from './go-cqhttp/SendForwardMsg';
|
||||
import GoCQHTTPGetStrangerInfo from './go-cqhttp/GetStrangerInfo';
|
||||
import SendLike from './user/SendLike';
|
||||
import SetGroupAddRequest from './group/SetGroupAddRequest';
|
||||
import SetGroupLeave from './group/SetGroupLeave';
|
||||
import GetGuildList from './group/GetGuildList';
|
||||
import Debug from './llonebot/Debug';
|
||||
import SetFriendAddRequest from './user/SetFriendAddRequest';
|
||||
import SetGroupWholeBan from './group/SetGroupWholeBan';
|
||||
import SetGroupName from './group/SetGroupName';
|
||||
import SetGroupBan from './group/SetGroupBan';
|
||||
import SetGroupKick from './group/SetGroupKick';
|
||||
import SetGroupAdmin from './group/SetGroupAdmin';
|
||||
import SetGroupCard from './group/SetGroupCard';
|
||||
import GetImage from './file/GetImage';
|
||||
import GetRecord from './file/GetRecord';
|
||||
import GoCQHTTPMarkMsgAsRead from './msg/MarkMsgAsRead';
|
||||
import CleanCache from './system/CleanCache';
|
||||
import GoCQHTTPUploadGroupFile from './go-cqhttp/UploadGroupFile';
|
||||
import { GetConfigAction, SetConfigAction } from './llonebot/Config';
|
||||
import GetGroupAddRequest from './llonebot/GetGroupAddRequest';
|
||||
import SetQQAvatar from './llonebot/SetQQAvatar';
|
||||
import GoCQHTTPDownloadFile from './go-cqhttp/DownloadFile';
|
||||
import GoCQHTTPGetGroupMsgHistory from './go-cqhttp/GetGroupMsgHistory';
|
||||
import GetFile from './file/GetFile';
|
||||
import { GoCQHTTGetForwardMsgAction } from './go-cqhttp/GetForwardMsg';
|
||||
|
||||
export const actionHandlers = [
|
||||
new GetFile(),
|
||||
new Debug(),
|
||||
// new GetConfigAction(),
|
||||
// new SetConfigAction(),
|
||||
// new GetGroupAddRequest(),
|
||||
new SetQQAvatar(),
|
||||
// onebot11
|
||||
new SendLike(),
|
||||
new GetMsg(),
|
||||
new GetLoginInfo(),
|
||||
new GetFriendList(),
|
||||
new GetGroupList(), new GetGroupInfo(),
|
||||
new GetGroupMemberList(), new GetGroupMemberInfo(),
|
||||
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
|
||||
new DeleteMsg(),
|
||||
new SetGroupAddRequest(),
|
||||
new SetFriendAddRequest(),
|
||||
new SetGroupLeave(),
|
||||
new GetVersionInfo(),
|
||||
new CanSendRecord(),
|
||||
new CanSendImage(),
|
||||
new GetStatus(),
|
||||
new SetGroupWholeBan(),
|
||||
new SetGroupBan(),
|
||||
new SetGroupKick(),
|
||||
new SetGroupAdmin(),
|
||||
new SetGroupName(),
|
||||
new SetGroupCard(),
|
||||
new GetImage(),
|
||||
new GetRecord(),
|
||||
// new CleanCache(),
|
||||
|
||||
//以下为go-cqhttp api
|
||||
new GoCQHTTPSendForwardMsg(),
|
||||
new GoCQHTTPSendGroupForwardMsg(),
|
||||
new GoCQHTTPSendPrivateForwardMsg(),
|
||||
new GoCQHTTPGetStrangerInfo(),
|
||||
new GoCQHTTPDownloadFile(),
|
||||
new GetGuildList(),
|
||||
new GoCQHTTPMarkMsgAsRead(),
|
||||
new GoCQHTTPUploadGroupFile(),
|
||||
new GoCQHTTPGetGroupMsgHistory(),
|
||||
new GoCQHTTGetForwardMsgAction(),
|
||||
|
||||
];
|
||||
|
||||
function initActionMap() {
|
||||
const actionMap = new Map<string, BaseAction<any, any>>();
|
||||
for (const action of actionHandlers) {
|
||||
actionMap.set(action.actionName, action);
|
||||
}
|
||||
|
||||
return actionMap;
|
||||
}
|
||||
|
||||
export const actionMap = initActionMap();
|
||||
20
src/onebot11/action/llonebot/Config.ts
Normal file
20
src/onebot11/action/llonebot/Config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { OB11Config, ob11Config } from '@/onebot11/config';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
|
||||
export class GetConfigAction extends BaseAction<null, OB11Config> {
|
||||
actionName = ActionName.GetConfig;
|
||||
|
||||
protected async _handle(payload: null): Promise<OB11Config> {
|
||||
return ob11Config;
|
||||
}
|
||||
}
|
||||
|
||||
export class SetConfigAction extends BaseAction<OB11Config, void> {
|
||||
actionName = ActionName.SetConfig;
|
||||
|
||||
protected async _handle(payload: OB11Config): Promise<void> {
|
||||
ob11Config.save(payload);
|
||||
}
|
||||
}
|
||||
44
src/onebot11/action/llonebot/Debug.ts
Normal file
44
src/onebot11/action/llonebot/Debug.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
// import * as ntqqApi from "../../../ntqqapi/api";
|
||||
import {
|
||||
NTQQMsgApi,
|
||||
NTQQFriendApi,
|
||||
NTQQGroupApi,
|
||||
NTQQUserApi,
|
||||
NTQQFileApi,
|
||||
// NTQQFileCacheApi,
|
||||
NTQQWindowApi,
|
||||
} from '@/core/qqnt/apis';
|
||||
import { ActionName } from '../types';
|
||||
import { log } from '../../../common/utils/log';
|
||||
|
||||
interface Payload {
|
||||
method: string,
|
||||
args: any[],
|
||||
}
|
||||
|
||||
export default class Debug extends BaseAction<Payload, any> {
|
||||
actionName = ActionName.Debug;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<any> {
|
||||
log('debug call ntqq api', payload);
|
||||
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi,
|
||||
// NTQQFileCacheApi,
|
||||
NTQQWindowApi];
|
||||
for (const ntqqApiClass of ntqqApi) {
|
||||
log('ntqqApiClass', ntqqApiClass);
|
||||
const method = (<any>ntqqApiClass)[payload.method];
|
||||
if (method) {
|
||||
const result = method(...payload.args);
|
||||
if (method.constructor.name === 'AsyncFunction') {
|
||||
return await result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
throw `${payload.method}方法 不存在`;
|
||||
|
||||
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
|
||||
// return info
|
||||
}
|
||||
}
|
||||
33
src/onebot11/action/llonebot/GetGroupAddRequest.ts
Normal file
33
src/onebot11/action/llonebot/GetGroupAddRequest.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { GroupNotify, GroupNotifyStatus } from '../../../ntqqapi/types';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { uid2UinMap } from '../../../common/data';
|
||||
import { NTQQUserApi } from '@/core/qqnt/apis/user';
|
||||
import { NTQQGroupApi } from '@/core/qqnt/apis/group';
|
||||
import { log } from '../../../common/utils/log';
|
||||
|
||||
interface OB11GroupRequestNotify {
|
||||
group_id: number,
|
||||
user_id: number,
|
||||
flag: string
|
||||
}
|
||||
|
||||
export default class GetGroupAddRequest extends BaseAction<null, OB11GroupRequestNotify[]> {
|
||||
actionName = ActionName.GetGroupIgnoreAddRequest;
|
||||
|
||||
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
|
||||
const data = await NTQQGroupApi.getGroupIgnoreNotifies();
|
||||
// log(data);
|
||||
const notifies: GroupNotify[] = data.notifies.filter(notify => notify.status === GroupNotifyStatus.WAIT_HANDLE);
|
||||
const returnData: OB11GroupRequestNotify[] = [];
|
||||
for (const notify of notifies) {
|
||||
const uin = uid2UinMap[notify.user1.uid] || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin;
|
||||
returnData.push({
|
||||
group_id: parseInt(notify.group.groupCode),
|
||||
user_id: parseInt(uin),
|
||||
flag: notify.seq
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
}
|
||||
43
src/onebot11/action/llonebot/SetQQAvatar.ts
Normal file
43
src/onebot11/action/llonebot/SetQQAvatar.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import * as fs from 'node:fs';
|
||||
import { NTQQUserApi } from '@/core/qqnt/apis/user';
|
||||
import { checkFileReceived, uri2local } from '../../../common/utils/file';
|
||||
// import { log } from "../../../common/utils";
|
||||
|
||||
interface Payload {
|
||||
file: string
|
||||
}
|
||||
|
||||
export default class SetAvatar extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetQQAvatar;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const { path, isLocal, errMsg } = (await uri2local(payload.file));
|
||||
if (errMsg){
|
||||
throw `头像${payload.file}设置失败,file字段可能格式不正确`;
|
||||
}
|
||||
if (path) {
|
||||
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断
|
||||
const ret = await NTQQUserApi.setQQAvatar(path);
|
||||
if (!isLocal){
|
||||
fs.unlink(path, () => {});
|
||||
}
|
||||
if (!ret) {
|
||||
throw `头像${payload.file}设置失败,api无返回`;
|
||||
}
|
||||
// log(`头像设置返回:${JSON.stringify(ret)}`)
|
||||
if (ret['result'] == 1004022) {
|
||||
throw `头像${payload.file}设置失败,文件可能不是图片格式`;
|
||||
} else if(ret['result'] != 0) {
|
||||
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`;
|
||||
}
|
||||
} else {
|
||||
if (!isLocal){
|
||||
fs.unlink(path, () => {});
|
||||
}
|
||||
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
22
src/onebot11/action/msg/DeleteMsg.ts
Normal file
22
src/onebot11/action/msg/DeleteMsg.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { NTQQMsgApi } from '@/core/qqnt/apis';
|
||||
import { ActionName } from '../types';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { napCatCore } from '@/core';
|
||||
|
||||
interface Payload {
|
||||
message_id: number
|
||||
}
|
||||
|
||||
class DeleteMsg extends BaseAction<Payload, void> {
|
||||
actionName = ActionName.DeleteMsg;
|
||||
|
||||
protected async _handle(payload: Payload) {
|
||||
const msg = await dbUtil.getMsgByShortId(payload.message_id);
|
||||
if (msg) {
|
||||
await NTQQMsgApi.recallMsg({ peerUid: msg.peerUid, chatType: msg.chatType }, [msg.msgId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DeleteMsg;
|
||||
33
src/onebot11/action/msg/GetMsg.ts
Normal file
33
src/onebot11/action/msg/GetMsg.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { OB11Message } from '../../types';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
|
||||
|
||||
export interface PayloadType {
|
||||
message_id: number
|
||||
}
|
||||
|
||||
export type ReturnDataType = OB11Message
|
||||
|
||||
class GetMsg extends BaseAction<PayloadType, OB11Message> {
|
||||
actionName = ActionName.GetMsg;
|
||||
|
||||
protected async _handle(payload: PayloadType) {
|
||||
// log("history msg ids", Object.keys(msgHistory));
|
||||
if (!payload.message_id) {
|
||||
throw ('参数message_id不能为空');
|
||||
}
|
||||
let msg = await dbUtil.getMsgByShortId(payload.message_id);
|
||||
if (!msg) {
|
||||
msg = await dbUtil.getMsgByLongId(payload.message_id.toString());
|
||||
}
|
||||
if (!msg) {
|
||||
throw ('消息不存在');
|
||||
}
|
||||
return await OB11Constructor.message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export default GetMsg;
|
||||
14
src/onebot11/action/msg/MarkMsgAsRead.ts
Normal file
14
src/onebot11/action/msg/MarkMsgAsRead.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
interface Payload{
|
||||
message_id: number
|
||||
}
|
||||
|
||||
export default class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null>{
|
||||
actionName = ActionName.GoCQHTTP_MarkMsgAsRead;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
532
src/onebot11/action/msg/SendMsg.ts
Normal file
532
src/onebot11/action/msg/SendMsg.ts
Normal file
@@ -0,0 +1,532 @@
|
||||
import {
|
||||
AtType,
|
||||
ChatType,
|
||||
ElementType,
|
||||
Group, PicSubType,
|
||||
RawMessage,
|
||||
SendArkElement,
|
||||
SendMessageElement,
|
||||
Peer
|
||||
} from '@/core/qqnt/entities';
|
||||
|
||||
import {
|
||||
OB11MessageCustomMusic,
|
||||
OB11MessageData,
|
||||
OB11MessageDataType,
|
||||
OB11MessageMixType,
|
||||
OB11MessageNode,
|
||||
OB11PostSendMsg
|
||||
} from '../../types';
|
||||
import { SendMsgElementConstructor } from '@/core/qqnt/entities/constructor';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName, BaseCheckResult } from '../types';
|
||||
import * as fs from 'node:fs';
|
||||
import { decodeCQCode } from '../../cqcode';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { log } from '@/common/utils/log';
|
||||
import { sleep } from '@/common/utils/helper';
|
||||
import { uri2local } from '@/common/utils/file';
|
||||
import { getFriend, getGroup, getGroupMember, getUidByUin, selfInfo } from '@/common/data';
|
||||
import { NTQQMsgApi } from '@/core/qqnt/apis/msg';
|
||||
|
||||
const ALLOW_SEND_TEMP_MSG = false;
|
||||
|
||||
function checkSendMessage(sendMsgList: OB11MessageData[]) {
|
||||
function checkUri(uri: string): boolean {
|
||||
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
|
||||
return pattern.test(uri);
|
||||
}
|
||||
|
||||
for (const msg of sendMsgList) {
|
||||
if (msg['type'] && msg['data']) {
|
||||
const type = msg['type'];
|
||||
const data = msg['data'];
|
||||
if (type === 'text' && !data['text']) {
|
||||
return 400;
|
||||
} else if (['image', 'voice', 'record'].includes(type)) {
|
||||
if (!data['file']) {
|
||||
return 400;
|
||||
} else {
|
||||
if (checkUri(data['file'])) {
|
||||
return 200;
|
||||
} else {
|
||||
return 400;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (type === 'at' && !data['qq']) {
|
||||
return 400;
|
||||
} else if (type === 'reply' && !data['id']) {
|
||||
return 400;
|
||||
}
|
||||
} else {
|
||||
return 400;
|
||||
}
|
||||
}
|
||||
return 200;
|
||||
}
|
||||
|
||||
export interface ReturnDataType {
|
||||
message_id: number;
|
||||
}
|
||||
|
||||
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
|
||||
if (typeof message === 'string') {
|
||||
if (!autoEscape) {
|
||||
message = decodeCQCode(message.toString());
|
||||
} else {
|
||||
message = [{
|
||||
type: OB11MessageDataType.text,
|
||||
data: {
|
||||
text: message
|
||||
}
|
||||
}];
|
||||
}
|
||||
} else if (!Array.isArray(message)) {
|
||||
message = [message];
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export async function createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) {
|
||||
const sendElements: SendMessageElement[] = [];
|
||||
const deleteAfterSentFiles: string[] = [];
|
||||
for (const sendMsg of messageData) {
|
||||
if (ignoreTypes.includes(sendMsg.type)) {
|
||||
continue;
|
||||
}
|
||||
switch (sendMsg.type) {
|
||||
case OB11MessageDataType.text: {
|
||||
const text = sendMsg.data?.text;
|
||||
if (text) {
|
||||
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case OB11MessageDataType.at: {
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
let atQQ = sendMsg.data?.qq;
|
||||
if (atQQ) {
|
||||
atQQ = atQQ.toString();
|
||||
if (atQQ === 'all') {
|
||||
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'));
|
||||
} else {
|
||||
// const atMember = group?.members.find(m => m.uin == atQQ)
|
||||
const atMember = await getGroupMember(group?.groupCode, atQQ);
|
||||
if (atMember) {
|
||||
sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case OB11MessageDataType.reply: {
|
||||
const replyMsgId = sendMsg.data.id;
|
||||
if (replyMsgId) {
|
||||
const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId));
|
||||
if (replyMsg) {
|
||||
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case OB11MessageDataType.face: {
|
||||
const faceId = sendMsg.data?.id;
|
||||
if (faceId) {
|
||||
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case OB11MessageDataType.image:
|
||||
case OB11MessageDataType.file:
|
||||
case OB11MessageDataType.video:
|
||||
case OB11MessageDataType.voice: {
|
||||
const file = sendMsg.data?.file;
|
||||
const payloadFileName = sendMsg.data?.name;
|
||||
if (file) {
|
||||
// todo: 使用缓存文件发送
|
||||
// const cache = await dbUtil.getFileCache(file);
|
||||
// if (cache) {
|
||||
// if (fs.existsSync(cache.filePath)) {
|
||||
// file = "file://" + cache.filePath;
|
||||
// } else if (cache.downloadFunc) {
|
||||
// await cache.downloadFunc();
|
||||
// file = cache.filePath;
|
||||
// } else if (cache.url) {
|
||||
// file = cache.url;
|
||||
// }
|
||||
// log("找到文件缓存", file);
|
||||
// }
|
||||
const {path, isLocal, fileName, errMsg} = (await uri2local(file));
|
||||
if (errMsg) {
|
||||
throw errMsg;
|
||||
}
|
||||
if (path) {
|
||||
if (!isLocal) { // 只删除http和base64转过来的文件
|
||||
deleteAfterSentFiles.push(path);
|
||||
}
|
||||
if (sendMsg.type === OB11MessageDataType.file) {
|
||||
log('发送文件', path, payloadFileName || fileName);
|
||||
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName));
|
||||
} else if (sendMsg.type === OB11MessageDataType.video) {
|
||||
log('发送视频', path, payloadFileName || fileName);
|
||||
let thumb = sendMsg.data?.thumb;
|
||||
if (thumb) {
|
||||
const uri2LocalRes = await uri2local(thumb);
|
||||
if (uri2LocalRes.success) {
|
||||
thumb = uri2LocalRes.path;
|
||||
}
|
||||
}
|
||||
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb));
|
||||
} else if (sendMsg.type === OB11MessageDataType.voice) {
|
||||
sendElements.push(await SendMsgElementConstructor.ptt(path));
|
||||
} else if (sendMsg.type === OB11MessageDataType.image) {
|
||||
sendElements.push(await SendMsgElementConstructor.pic(path, sendMsg.data.summary || '', <PicSubType>parseInt(sendMsg.data?.subType?.toString() || '0')));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case OB11MessageDataType.json: {
|
||||
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
sendElements,
|
||||
deleteAfterSentFiles
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
|
||||
if (!sendElements.length) {
|
||||
throw ('消息体无法解析');
|
||||
}
|
||||
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000);
|
||||
try {
|
||||
returnMsg.id = await dbUtil.addMsg(returnMsg, false);
|
||||
} catch (e: any) {
|
||||
log('发送消息id获取失败', e);
|
||||
returnMsg.id = 0;
|
||||
}
|
||||
// log('消息发送结果', returnMsg);
|
||||
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
|
||||
}));
|
||||
return returnMsg;
|
||||
}
|
||||
|
||||
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
|
||||
actionName = ActionName.SendMsg;
|
||||
|
||||
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||
const messages = convertMessage2List(payload.message);
|
||||
const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node);
|
||||
if (fmNum && fmNum != messages.length) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素'
|
||||
};
|
||||
}
|
||||
if (payload.message_type !== 'private' && payload.group_id && !(await getGroup(payload.group_id))) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `群${payload.group_id}不存在`
|
||||
};
|
||||
}
|
||||
if (payload.user_id && payload.message_type !== 'group') {
|
||||
if (!(await getFriend(payload.user_id))) {
|
||||
if (!ALLOW_SEND_TEMP_MSG
|
||||
&& !(await dbUtil.getUidByTempUin(payload.user_id.toString()))
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '不能发送临时消息'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
protected async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> {
|
||||
|
||||
const peer: Peer = {
|
||||
chatType: ChatType.friend,
|
||||
peerUid: ''
|
||||
};
|
||||
let isTempMsg = false;
|
||||
let group: Group | undefined = undefined;
|
||||
const genGroupPeer = async () => {
|
||||
if (payload.group_id) {
|
||||
group = await getGroup(payload.group_id.toString());
|
||||
if (group) {
|
||||
peer.chatType = ChatType.group;
|
||||
// peer.name = group.name
|
||||
peer.peerUid = group.groupCode;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const genFriendPeer = async () => {
|
||||
if (!payload.user_id) {
|
||||
return;
|
||||
}
|
||||
const friend = await getFriend(payload.user_id.toString());
|
||||
if (friend) {
|
||||
// peer.name = friend.nickName
|
||||
peer.peerUid = friend.uid;
|
||||
} else {
|
||||
peer.chatType = ChatType.temp;
|
||||
const tempUserUid = getUidByUin(payload.user_id.toString());
|
||||
if (!tempUserUid) {
|
||||
throw (`找不到私聊对象${payload.user_id}`);
|
||||
}
|
||||
// peer.name = tempUser.nickName
|
||||
isTempMsg = true;
|
||||
peer.peerUid = tempUserUid;
|
||||
}
|
||||
};
|
||||
if (payload?.group_id && payload.message_type === 'group') {
|
||||
await genGroupPeer();
|
||||
|
||||
} else if (payload?.user_id) {
|
||||
await genFriendPeer();
|
||||
} else if (payload.group_id) {
|
||||
await genGroupPeer();
|
||||
} else {
|
||||
throw ('发送消息参数错误, 请指定group_id或user_id');
|
||||
}
|
||||
const messages = convertMessage2List(payload.message);
|
||||
if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) {
|
||||
try {
|
||||
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group);
|
||||
return {message_id: returnMsg!.id!};
|
||||
} catch (e: any) {
|
||||
throw ('发送转发消息失败 ' + e.toString());
|
||||
}
|
||||
} else {
|
||||
if (this.getSpecialMsgNum(payload, OB11MessageDataType.music)) {
|
||||
const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic;
|
||||
if (music) {
|
||||
const {url, audio, title, content, image} = music.data;
|
||||
const selfPeer: Peer = {peerUid: selfInfo.uid, chatType: ChatType.friend};
|
||||
// 搞不定!
|
||||
// const musicMsg = await this.send(selfPeer, [this.genMusicElement(url, audio, title, content, image)], [], false)
|
||||
// 转发
|
||||
// const res = await NTQQApi.forwardMsg(selfPeer, peer, [musicMsg.msgId])
|
||||
// log("转发音乐消息成功", res);
|
||||
// return {message_id: musicMsg.msgShortId}
|
||||
}
|
||||
}
|
||||
}
|
||||
// log("send msg:", peer, sendElements)
|
||||
const {sendElements, deleteAfterSentFiles} = await createSendElements(messages, group);
|
||||
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles);
|
||||
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
|
||||
}));
|
||||
|
||||
const res = {message_id: returnMsg.id!};
|
||||
// console.log(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
|
||||
if (Array.isArray(payload.message)) {
|
||||
return payload.message.filter(msg => msg.type == msgType).length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
|
||||
log('克隆的目标消息', msg);
|
||||
const sendElements: SendMessageElement[] = [];
|
||||
for (const ele of msg.elements) {
|
||||
sendElements.push(ele as SendMessageElement);
|
||||
// Object.keys(ele).forEach((eleKey) => {
|
||||
// if (eleKey.endsWith("Element")) {
|
||||
// }
|
||||
|
||||
}
|
||||
if (sendElements.length === 0) {
|
||||
log('需要clone的消息无法解析,将会忽略掉', msg);
|
||||
}
|
||||
log('克隆消息', sendElements);
|
||||
try {
|
||||
const nodeMsg = await NTQQMsgApi.sendMsg({
|
||||
chatType: ChatType.friend,
|
||||
peerUid: selfInfo.uid
|
||||
}, sendElements, true);
|
||||
await sleep(500);
|
||||
return nodeMsg;
|
||||
} catch (e) {
|
||||
log(e, '克隆转发消息失败,将忽略本条消息', msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 返回一个合并转发的消息id
|
||||
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined): Promise<RawMessage | null> {
|
||||
|
||||
const selfPeer = {
|
||||
chatType: ChatType.friend,
|
||||
peerUid: selfInfo.uid
|
||||
};
|
||||
let nodeMsgIds: string[] = [];
|
||||
// 先判断一遍是不是id和自定义混用
|
||||
const needClone = messageNodes.filter(node => node.data.id).length && messageNodes.filter(node => !node.data.id).length;
|
||||
for (const messageNode of messageNodes) {
|
||||
// 一个node表示一个人的消息
|
||||
const nodeId = messageNode.data.id;
|
||||
// 有nodeId表示一个子转发消息卡片
|
||||
if (nodeId) {
|
||||
const nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId));
|
||||
if (!needClone) {
|
||||
nodeMsgIds.push(nodeMsg!.msgId);
|
||||
} else {
|
||||
if (nodeMsg!.peerUid !== selfInfo.uid) {
|
||||
const cloneMsg = await this.cloneMsg(nodeMsg!);
|
||||
if (cloneMsg) {
|
||||
nodeMsgIds.push(cloneMsg.msgId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 自定义的消息
|
||||
// 提取消息段,发给自己生成消息id
|
||||
try {
|
||||
const {
|
||||
sendElements,
|
||||
deleteAfterSentFiles
|
||||
} = await createSendElements(convertMessage2List(messageNode.data.content), group);
|
||||
log('开始生成转发节点', sendElements);
|
||||
const sendElementsSplit: SendMessageElement[][] = [];
|
||||
let splitIndex = 0;
|
||||
for (const ele of sendElements) {
|
||||
if (!sendElementsSplit[splitIndex]) {
|
||||
sendElementsSplit[splitIndex] = [];
|
||||
}
|
||||
|
||||
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
|
||||
if (sendElementsSplit[splitIndex].length > 0) {
|
||||
splitIndex++;
|
||||
}
|
||||
sendElementsSplit[splitIndex] = [ele];
|
||||
splitIndex++;
|
||||
} else {
|
||||
sendElementsSplit[splitIndex].push(ele);
|
||||
}
|
||||
log(sendElementsSplit);
|
||||
}
|
||||
// log("分割后的转发节点", sendElementsSplit)
|
||||
for (const eles of sendElementsSplit) {
|
||||
const nodeMsg = await sendMsg(selfPeer, eles, [], true);
|
||||
nodeMsgIds.push(nodeMsg.msgId);
|
||||
await sleep(500);
|
||||
log('转发节点生成成功', nodeMsg.msgId);
|
||||
}
|
||||
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
|
||||
}));
|
||||
|
||||
} catch (e) {
|
||||
log('生成转发消息节点失败', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查srcPeer是否一致,不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的,使其保持一致才能够转发
|
||||
const nodeMsgArray: Array<RawMessage> = [];
|
||||
let srcPeer: Peer | undefined = undefined;
|
||||
let needSendSelf = false;
|
||||
for (const [index, msgId] of nodeMsgIds.entries()) {
|
||||
const nodeMsg = await dbUtil.getMsgByLongId(msgId);
|
||||
if (nodeMsg) {
|
||||
nodeMsgArray.push(nodeMsg);
|
||||
if (!srcPeer) {
|
||||
srcPeer = {chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid};
|
||||
} else if (srcPeer.peerUid !== nodeMsg.peerUid) {
|
||||
needSendSelf = true;
|
||||
srcPeer = selfPeer;
|
||||
}
|
||||
}
|
||||
}
|
||||
log('nodeMsgArray', nodeMsgArray);
|
||||
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
|
||||
if (needSendSelf) {
|
||||
log('需要克隆转发消息');
|
||||
for (const [index, msg] of nodeMsgArray.entries()) {
|
||||
if (msg.peerUid !== selfInfo.uid) {
|
||||
const cloneMsg = await this.cloneMsg(msg);
|
||||
if (cloneMsg) {
|
||||
nodeMsgIds[index] = cloneMsg.msgId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// elements之间用换行符分隔
|
||||
// let _sendForwardElements: SendMessageElement[] = []
|
||||
// for(let i = 0; i < sendForwardElements.length; i++){
|
||||
// _sendForwardElements.push(sendForwardElements[i])
|
||||
// _sendForwardElements.push(SendMsgElementConstructor.text("\n\n"))
|
||||
// }
|
||||
// const nodeMsg = await NTQQApi.sendMsg(selfPeer, _sendForwardElements, true);
|
||||
// nodeIds.push(nodeMsg.msgId)
|
||||
// await sleep(500);
|
||||
// 开发转发
|
||||
try {
|
||||
log('开发转发', nodeMsgIds);
|
||||
return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, nodeMsgIds);
|
||||
} catch (e) {
|
||||
log('forward failed', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement {
|
||||
const musicJson = {
|
||||
app: 'com.tencent.structmsg',
|
||||
config: {
|
||||
ctime: 1709689928,
|
||||
forward: 1,
|
||||
token: '5c1e4905f926dd3a64a4bd3841460351',
|
||||
type: 'normal'
|
||||
},
|
||||
extra: {app_type: 1, appid: 100497308, uin: selfInfo.uin},
|
||||
meta: {
|
||||
news: {
|
||||
action: '',
|
||||
android_pkg_name: '',
|
||||
app_type: 1,
|
||||
appid: 100497308,
|
||||
ctime: 1709689928,
|
||||
desc: content || title,
|
||||
jumpUrl: url,
|
||||
musicUrl: audio,
|
||||
preview: image,
|
||||
source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
|
||||
source_url: '',
|
||||
tag: 'QQ音乐',
|
||||
title: title,
|
||||
uin: selfInfo.uin,
|
||||
}
|
||||
},
|
||||
prompt: content || title,
|
||||
ver: '0.0.0.1',
|
||||
view: 'news'
|
||||
};
|
||||
|
||||
return SendMsgElementConstructor.ark(musicJson);
|
||||
}
|
||||
}
|
||||
|
||||
export default SendMsg;
|
||||
14
src/onebot11/action/msg/SendPrivateMsg.ts
Normal file
14
src/onebot11/action/msg/SendPrivateMsg.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import SendMsg from './SendMsg';
|
||||
import { ActionName, BaseCheckResult } from '../types';
|
||||
import { OB11PostSendMsg } from '../../types';
|
||||
|
||||
class SendPrivateMsg extends SendMsg {
|
||||
actionName = ActionName.SendPrivateMsg;
|
||||
|
||||
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
|
||||
payload.message_type = 'private';
|
||||
return super.check(payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default SendPrivateMsg;
|
||||
10
src/onebot11/action/system/CanSendImage.ts
Normal file
10
src/onebot11/action/system/CanSendImage.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ActionName } from '../types';
|
||||
import CanSendRecord from './CanSendRecord';
|
||||
|
||||
interface ReturnType {
|
||||
yes: boolean
|
||||
}
|
||||
|
||||
export default class CanSendImage extends CanSendRecord {
|
||||
actionName = ActionName.CanSendImage;
|
||||
}
|
||||
16
src/onebot11/action/system/CanSendRecord.ts
Normal file
16
src/onebot11/action/system/CanSendRecord.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
interface ReturnType {
|
||||
yes: boolean
|
||||
}
|
||||
|
||||
export default class CanSendRecord extends BaseAction<any, ReturnType> {
|
||||
actionName = ActionName.CanSendRecord;
|
||||
|
||||
protected async _handle(payload): Promise<ReturnType> {
|
||||
return {
|
||||
yes: true
|
||||
};
|
||||
}
|
||||
}
|
||||
105
src/onebot11/action/system/CleanCache.ts
Normal file
105
src/onebot11/action/system/CleanCache.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import fs from 'fs';
|
||||
import Path from 'path';
|
||||
import {
|
||||
ChatType,
|
||||
ChatCacheListItemBasic,
|
||||
CacheFileType
|
||||
} from '../../../ntqqapi/types';
|
||||
import { dbUtil } from '../../../common/db';
|
||||
import { NTQQFileApi, NTQQFileCacheApi } from '@/core/qqnt/apis/file';
|
||||
|
||||
export default class CleanCache extends BaseAction<void, void> {
|
||||
actionName = ActionName.CleanCache;
|
||||
|
||||
protected _handle(): Promise<void> {
|
||||
return new Promise<void>(async (res, rej) => {
|
||||
try {
|
||||
// dbUtil.clearCache();
|
||||
const cacheFilePaths: string[] = [];
|
||||
|
||||
await NTQQFileCacheApi.setCacheSilentScan(false);
|
||||
|
||||
cacheFilePaths.push((await NTQQFileCacheApi.getHotUpdateCachePath()));
|
||||
cacheFilePaths.push((await NTQQFileCacheApi.getDesktopTmpPath()));
|
||||
(await NTQQFileCacheApi.getCacheSessionPathList()).forEach(e => cacheFilePaths.push(e.value));
|
||||
|
||||
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
|
||||
const cacheScanResult = await NTQQFileCacheApi.scanCache();
|
||||
const cacheSize = parseInt(cacheScanResult.size[6]);
|
||||
|
||||
if (cacheScanResult.result !== 0) {
|
||||
throw('Something went wrong while scanning cache. Code: ' + cacheScanResult.result);
|
||||
}
|
||||
|
||||
await NTQQFileCacheApi.setCacheSilentScan(true);
|
||||
if (cacheSize > 0 && cacheFilePaths.length > 2) { // 存在缓存文件且大小不为 0 时执行清理动作
|
||||
// await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了
|
||||
deleteCachePath(cacheFilePaths);
|
||||
}
|
||||
|
||||
// 获取聊天记录列表
|
||||
// NOTE: 以防有人不需要删除聊天记录,暂时先注释掉,日后加个开关
|
||||
// const privateChatCache = await getCacheList(ChatType.friend); // 私聊消息
|
||||
// const groupChatCache = await getCacheList(ChatType.group); // 群聊消息
|
||||
// const chatCacheList = [ ...privateChatCache, ...groupChatCache ];
|
||||
const chatCacheList: ChatCacheListItemBasic[] = [];
|
||||
|
||||
// 获取聊天缓存文件列表
|
||||
const cacheFileList: string[] = [];
|
||||
|
||||
for (const name in CacheFileType) {
|
||||
if (!isNaN(parseInt(name))) continue;
|
||||
|
||||
const fileTypeAny: any = CacheFileType[name];
|
||||
const fileType: CacheFileType = fileTypeAny;
|
||||
|
||||
cacheFileList.push(...(await NTQQFileCacheApi.getFileCacheInfo(fileType)).infos.map(file => file.fileKey));
|
||||
}
|
||||
|
||||
// 一并清除
|
||||
await NTQQFileCacheApi.clearChatCache(chatCacheList, cacheFileList);
|
||||
res();
|
||||
} catch(e) {
|
||||
console.error('清理缓存时发生了错误');
|
||||
rej(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deleteCachePath(pathList: string[]) {
|
||||
const emptyPath = (path: string) => {
|
||||
if (!fs.existsSync(path)) return;
|
||||
const files = fs.readdirSync(path);
|
||||
files.forEach(file => {
|
||||
const filePath = Path.resolve(path, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.isDirectory()) emptyPath(filePath);
|
||||
else fs.unlinkSync(filePath);
|
||||
});
|
||||
fs.rmdirSync(path);
|
||||
};
|
||||
|
||||
for (const path of pathList) {
|
||||
emptyPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheList(type: ChatType) { // NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理
|
||||
return new Promise<Array<ChatCacheListItemBasic>>((res, rej) => {
|
||||
NTQQFileCacheApi.getChatCacheList(type, 1000, 0)
|
||||
.then(data => {
|
||||
const list = data.infos.filter(e => e.chatType === type && parseInt(e.basicChatCacheInfo.chatSize) > 0);
|
||||
const result = list.map(e => {
|
||||
const result = { ...e.basicChatCacheInfo };
|
||||
result.chatType = type;
|
||||
result.isChecked = true;
|
||||
return result;
|
||||
});
|
||||
res(result);
|
||||
})
|
||||
.catch(e => rej(e));
|
||||
});
|
||||
}
|
||||
17
src/onebot11/action/system/GetLoginInfo.ts
Normal file
17
src/onebot11/action/system/GetLoginInfo.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { selfInfo } from '@/common/data';
|
||||
import { OB11User } from '../../types';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { napCatCore } from '@/core';
|
||||
|
||||
|
||||
class GetLoginInfo extends BaseAction<null, OB11User> {
|
||||
actionName = ActionName.GetLoginInfo;
|
||||
|
||||
protected async _handle(payload: null) {
|
||||
return OB11Constructor.selfInfo(selfInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export default GetLoginInfo;
|
||||
16
src/onebot11/action/system/GetStatus.ts
Normal file
16
src/onebot11/action/system/GetStatus.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { OB11Status } from '../../types';
|
||||
import { ActionName } from '../types';
|
||||
import { selfInfo } from '../../../common/data';
|
||||
|
||||
|
||||
export default class GetStatus extends BaseAction<any, OB11Status> {
|
||||
actionName = ActionName.GetStatus;
|
||||
|
||||
protected async _handle(payload: any): Promise<OB11Status> {
|
||||
return {
|
||||
online: !!selfInfo.online,
|
||||
good: true
|
||||
};
|
||||
}
|
||||
}
|
||||
16
src/onebot11/action/system/GetVersionInfo.ts
Normal file
16
src/onebot11/action/system/GetVersionInfo.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { OB11Version } from '../../types';
|
||||
import { ActionName } from '../types';
|
||||
import { version } from '@/onebot11/version';
|
||||
|
||||
export default class GetVersionInfo extends BaseAction<any, OB11Version> {
|
||||
actionName = ActionName.GetVersionInfo;
|
||||
|
||||
protected async _handle(payload: any): Promise<OB11Version> {
|
||||
return {
|
||||
app_name: 'NapCat.Onebot',
|
||||
protocol_version: 'v11',
|
||||
app_version: version
|
||||
};
|
||||
}
|
||||
}
|
||||
64
src/onebot11/action/types.ts
Normal file
64
src/onebot11/action/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export type BaseCheckResult = ValidCheckResult | InvalidCheckResult
|
||||
|
||||
export interface ValidCheckResult {
|
||||
valid: true
|
||||
|
||||
[k: string | number]: any
|
||||
}
|
||||
|
||||
export interface InvalidCheckResult {
|
||||
valid: false
|
||||
message: string
|
||||
|
||||
[k: string | number]: any
|
||||
}
|
||||
|
||||
export enum ActionName {
|
||||
// llonebot
|
||||
GetGroupIgnoreAddRequest = 'get_group_ignore_add_request',
|
||||
SetQQAvatar = 'set_qq_avatar',
|
||||
GetConfig = 'get_config',
|
||||
SetConfig = 'set_config',
|
||||
Debug = 'llonebot_debug',
|
||||
GetFile = 'get_file',
|
||||
// onebot 11
|
||||
SendLike = 'send_like',
|
||||
GetLoginInfo = 'get_login_info',
|
||||
GetFriendList = 'get_friend_list',
|
||||
GetGroupInfo = 'get_group_info',
|
||||
GetGroupList = 'get_group_list',
|
||||
GetGroupMemberInfo = 'get_group_member_info',
|
||||
GetGroupMemberList = 'get_group_member_list',
|
||||
GetMsg = 'get_msg',
|
||||
SendMsg = 'send_msg',
|
||||
SendGroupMsg = 'send_group_msg',
|
||||
SendPrivateMsg = 'send_private_msg',
|
||||
DeleteMsg = 'delete_msg',
|
||||
SetGroupAddRequest = 'set_group_add_request',
|
||||
SetFriendAddRequest = 'set_friend_add_request',
|
||||
SetGroupLeave = 'set_group_leave',
|
||||
GetVersionInfo = 'get_version_info',
|
||||
GetStatus = 'get_status',
|
||||
CanSendRecord = 'can_send_record',
|
||||
CanSendImage = 'can_send_image',
|
||||
SetGroupKick = 'set_group_kick',
|
||||
SetGroupBan = 'set_group_ban',
|
||||
SetGroupWholeBan = 'set_group_whole_ban',
|
||||
SetGroupAdmin = 'set_group_admin',
|
||||
SetGroupCard = 'set_group_card',
|
||||
SetGroupName = 'set_group_name',
|
||||
GetImage = 'get_image',
|
||||
GetRecord = 'get_record',
|
||||
CleanCache = 'clean_cache',
|
||||
// 以下为go-cqhttp api
|
||||
GoCQHTTP_SendForwardMsg = 'send_forward_msg',
|
||||
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
|
||||
GoCQHTTP_SendPrivateForwardMsg = 'send_private_forward_msg',
|
||||
GoCQHTTP_GetStrangerInfo = 'get_stranger_info',
|
||||
GetGuildList = 'get_guild_list',
|
||||
GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read',
|
||||
GoCQHTTP_UploadGroupFile = 'upload_group_file',
|
||||
GoCQHTTP_DownloadFile = 'download_file',
|
||||
GoCQHTTP_GetGroupMsgHistory = 'get_group_msg_history',
|
||||
GoCQHTTP_GetForwardMsg = 'get_forward_msg',
|
||||
}
|
||||
16
src/onebot11/action/user/GetFriendList.ts
Normal file
16
src/onebot11/action/user/GetFriendList.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { OB11User } from '../../types';
|
||||
import { OB11Constructor } from '../../constructor';
|
||||
import { friends } from '../../../common/data';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
|
||||
|
||||
class GetFriendList extends BaseAction<null, OB11User[]> {
|
||||
actionName = ActionName.GetFriendList;
|
||||
|
||||
protected async _handle(payload: null) {
|
||||
return OB11Constructor.friends(Array.from(friends.values()));
|
||||
}
|
||||
}
|
||||
|
||||
export default GetFriendList;
|
||||
36
src/onebot11/action/user/SendLike.ts
Normal file
36
src/onebot11/action/user/SendLike.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NTQQUserApi } from '@/core/qqnt/apis';
|
||||
import BaseAction from '../BaseAction';
|
||||
import { getFriend, getUidByUin, uid2UinMap } from '../../../common/data';
|
||||
import { ActionName } from '../types';
|
||||
import { log } from '../../../common/utils/log';
|
||||
|
||||
interface Payload {
|
||||
user_id: number,
|
||||
times: number
|
||||
}
|
||||
|
||||
export default class SendLike extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SendLike;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
log('点赞参数', payload);
|
||||
try {
|
||||
const qq = payload.user_id.toString();
|
||||
const friend = await getFriend(qq);
|
||||
let uid: string;
|
||||
if (!friend) {
|
||||
uid = getUidByUin(qq) || '';
|
||||
} else {
|
||||
uid = friend.uid;
|
||||
}
|
||||
const result = await NTQQUserApi.like(uid, parseInt(payload.times?.toString()) || 1);
|
||||
console.log('点赞结果', result);
|
||||
if (result.result !== 0) {
|
||||
throw Error(result.errMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
throw `点赞失败 ${e}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
21
src/onebot11/action/user/SetFriendAddRequest.ts
Normal file
21
src/onebot11/action/user/SetFriendAddRequest.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import BaseAction from '../BaseAction';
|
||||
import { ActionName } from '../types';
|
||||
import { NTQQFriendApi } from '@/core/qqnt/apis/friend';
|
||||
import { friendRequests } from '@/common/data';
|
||||
|
||||
interface Payload {
|
||||
flag: string,
|
||||
approve: boolean,
|
||||
remark?: string,
|
||||
}
|
||||
|
||||
export default class SetFriendAddRequest extends BaseAction<Payload, null> {
|
||||
actionName = ActionName.SetFriendAddRequest;
|
||||
|
||||
protected async _handle(payload: Payload): Promise<null> {
|
||||
const approve = payload.approve.toString() === 'true';
|
||||
const request = friendRequests[payload.flag];
|
||||
await NTQQFriendApi.handleFriendRequest(request, approve);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
80
src/onebot11/config.ts
Normal file
80
src/onebot11/config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { selfInfo } from '@/common/data';
|
||||
|
||||
export interface OB11Config {
|
||||
httpPort: number;
|
||||
httpPostUrls: string[];
|
||||
httpSecret: string;
|
||||
wsPort: number;
|
||||
wsReverseUrls: string[];
|
||||
enableHttp: boolean;
|
||||
enableHttpPost: boolean;
|
||||
enableWs: boolean;
|
||||
enableWsReverse: boolean;
|
||||
messagePostFormat: 'array' | 'string';
|
||||
reportSelfMessage: boolean;
|
||||
enableLocalFile2Url: boolean;
|
||||
debug: boolean;
|
||||
heartInterval: number;
|
||||
token: string;
|
||||
|
||||
read(): OB11Config;
|
||||
|
||||
save(config: OB11Config): void;
|
||||
}
|
||||
|
||||
const ob11ConfigDir = path.resolve(__dirname, 'config');
|
||||
fs.mkdirSync(ob11ConfigDir, { recursive: true });
|
||||
|
||||
class Config implements OB11Config {
|
||||
httpPort: number = 3000;
|
||||
httpPostUrls: string[] = [];
|
||||
httpSecret = '';
|
||||
wsPort = 3001;
|
||||
wsReverseUrls: string[] = [];
|
||||
enableHttp = false;
|
||||
enableHttpPost = false;
|
||||
enableWs = false;
|
||||
enableWsReverse = false;
|
||||
messagePostFormat: 'array' | 'string' = 'array';
|
||||
reportSelfMessage = false;
|
||||
debug = false;
|
||||
enableLocalFile2Url = true;
|
||||
heartInterval = 30000;
|
||||
token = '';
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
getConfigPath() {
|
||||
return path.join(ob11ConfigDir, `onebot11_${selfInfo.uin}.json`);
|
||||
}
|
||||
|
||||
read() {
|
||||
const ob11ConfigPath = this.getConfigPath();
|
||||
if (!fs.existsSync(ob11ConfigPath)) {
|
||||
console.log(`onebot11配置文件 ${ob11ConfigPath} 不存在, 现已创建,请修改配置文件后重启NapCat`);
|
||||
this.save();
|
||||
return this;
|
||||
}
|
||||
const data = fs.readFileSync(ob11ConfigPath, 'utf-8');
|
||||
try {
|
||||
const jsonData = JSON.parse(data);
|
||||
console.log('get config', jsonData);
|
||||
Object.assign(this, jsonData);
|
||||
// eslint-disable-next-line
|
||||
} catch (e) {
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
save(newConfig: OB11Config | null = null) {
|
||||
if (newConfig) {
|
||||
Object.assign(this, newConfig);
|
||||
}
|
||||
fs.writeFileSync(this.getConfigPath(), JSON.stringify(this, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
export const ob11Config = new Config();
|
||||
486
src/onebot11/constructor.ts
Normal file
486
src/onebot11/constructor.ts
Normal file
@@ -0,0 +1,486 @@
|
||||
import {
|
||||
OB11Group,
|
||||
OB11GroupMember,
|
||||
OB11GroupMemberRole,
|
||||
OB11Message,
|
||||
OB11MessageData,
|
||||
OB11MessageDataType,
|
||||
OB11User,
|
||||
OB11UserSex
|
||||
} from './types';
|
||||
import {
|
||||
AtType,
|
||||
ChatType,
|
||||
ElementType,
|
||||
Friend,
|
||||
GrayTipElementSubType,
|
||||
Group,
|
||||
GroupMember,
|
||||
IMAGE_HTTP_HOST,
|
||||
RawMessage,
|
||||
SelfInfo,
|
||||
Sex,
|
||||
TipGroupElementType,
|
||||
User
|
||||
} from '@/core/qqnt/entities';
|
||||
import { EventType } from './event/OB11BaseEvent';
|
||||
import { encodeCQCode } from './cqcode';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent';
|
||||
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent';
|
||||
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent';
|
||||
import { OB11GroupNoticeEvent } from './event/notice/OB11GroupNoticeEvent';
|
||||
|
||||
import { calcQQLevel } from '../common/utils/qqlevel';
|
||||
import { log } from '../common/utils/log';
|
||||
import { sleep } from '../common/utils/helper';
|
||||
import { OB11GroupTitleEvent } from './event/notice/OB11GroupTitleEvent';
|
||||
import { OB11GroupCardEvent } from './event/notice/OB11GroupCardEvent';
|
||||
import { OB11GroupDecreaseEvent } from './event/notice/OB11GroupDecreaseEvent';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
import { getFriend, getGroupMember, groupMembers, selfInfo, tempGroupCodeMap } from '@/common/data';
|
||||
|
||||
|
||||
export class OB11Constructor {
|
||||
static async message(msg: RawMessage): Promise<OB11Message> {
|
||||
const { messagePostFormat } = ob11Config;
|
||||
const message_type = msg.chatType == ChatType.group ? 'group' : 'private';
|
||||
const resMsg: OB11Message = {
|
||||
self_id: parseInt(selfInfo.uin),
|
||||
user_id: parseInt(msg.senderUin!),
|
||||
time: parseInt(msg.msgTime) || Date.now(),
|
||||
message_id: msg.id!,
|
||||
real_id: msg.id!,
|
||||
message_type: msg.chatType == ChatType.group ? 'group' : 'private',
|
||||
sender: {
|
||||
user_id: parseInt(msg.senderUin!),
|
||||
nickname: msg.sendNickName,
|
||||
card: msg.sendMemberName || '',
|
||||
},
|
||||
raw_message: '',
|
||||
font: 14,
|
||||
sub_type: 'friend',
|
||||
message: messagePostFormat === 'string' ? '' : [],
|
||||
message_format: messagePostFormat === 'string' ? 'string' : 'array',
|
||||
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
|
||||
};
|
||||
if (msg.chatType == ChatType.group) {
|
||||
resMsg.sub_type = 'normal'; // 这里go-cqhttp是group,而onebot11标准是normal, 蛋疼
|
||||
resMsg.group_id = parseInt(msg.peerUin);
|
||||
const member = await getGroupMember(msg.peerUin, msg.senderUin!);
|
||||
if (member) {
|
||||
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role);
|
||||
resMsg.sender.nickname = member.nick;
|
||||
}
|
||||
} else if (msg.chatType == ChatType.friend) {
|
||||
resMsg.sub_type = 'friend';
|
||||
const friend = await getFriend(msg.senderUin!);
|
||||
if (friend) {
|
||||
resMsg.sender.nickname = friend.nick;
|
||||
}
|
||||
} else if (msg.chatType == ChatType.temp) {
|
||||
resMsg.sub_type = 'group';
|
||||
const tempGroupCode = tempGroupCodeMap[msg.peerUin];
|
||||
if (tempGroupCode) {
|
||||
resMsg.group_id = parseInt(tempGroupCode);
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of msg.elements) {
|
||||
const message_data: OB11MessageData | any = {
|
||||
data: {},
|
||||
type: 'unknown'
|
||||
};
|
||||
if (element.textElement && element.textElement?.atType !== AtType.notAt) {
|
||||
message_data['type'] = OB11MessageDataType.at;
|
||||
if (element.textElement.atType == AtType.atAll) {
|
||||
// message_data["data"]["mention"] = "all"
|
||||
message_data['data']['qq'] = 'all';
|
||||
} else {
|
||||
const atUid = element.textElement.atNtUid;
|
||||
let atQQ = element.textElement.atUid;
|
||||
if (!atQQ || atQQ === '0') {
|
||||
const atMember = await getGroupMember(msg.peerUin, atUid);
|
||||
if (atMember) {
|
||||
atQQ = atMember.uin;
|
||||
}
|
||||
}
|
||||
if (atQQ) {
|
||||
// message_data["data"]["mention"] = atQQ
|
||||
message_data['data']['qq'] = atQQ;
|
||||
}
|
||||
}
|
||||
} else if (element.textElement) {
|
||||
message_data['type'] = 'text';
|
||||
const text = element.textElement.content;
|
||||
if (!text.trim()) {
|
||||
continue;
|
||||
}
|
||||
message_data['data']['text'] = text;
|
||||
} else if (element.replyElement) {
|
||||
message_data['type'] = 'reply';
|
||||
// log("收到回复消息", element.replyElement.replayMsgSeq)
|
||||
try {
|
||||
const replyMsg = await dbUtil.getMsgBySeq(msg.peerUid, element.replyElement.replayMsgSeq);
|
||||
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
|
||||
if (replyMsg && replyMsg.id) {
|
||||
message_data['data']['id'] = replyMsg.id!.toString();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} catch (e: any) {
|
||||
log('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq);
|
||||
}
|
||||
|
||||
} else if (element.picElement) {
|
||||
message_data['type'] = 'image';
|
||||
// message_data["data"]["file"] = element.picElement.sourcePath
|
||||
message_data['data']['file'] = element.picElement.fileName;
|
||||
// message_data["data"]["path"] = element.picElement.sourcePath
|
||||
const url = element.picElement.originImageUrl;
|
||||
const md5HexStr = element.picElement.md5HexStr;
|
||||
const fileMd5 = element.picElement.md5HexStr;
|
||||
const fileUuid = element.picElement.fileUuid;
|
||||
// let currentRKey = config.imageRKey || "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
|
||||
const currentRKey = 'CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64';
|
||||
if (url) {
|
||||
if (url.startsWith('/download')) {
|
||||
if (url.includes('&rkey=')) {
|
||||
// 正则提取rkey
|
||||
// const rkey = url.match(/&rkey=([^&]+)/)[1]
|
||||
// // log("图片url已有rkey", rkey)
|
||||
// if (rkey != currentRKey){
|
||||
// config.imageRKey = rkey
|
||||
// if (Date.now() - lastRKeyUpdateTime > 1000 * 60) {
|
||||
// lastRKeyUpdateTime = Date.now()
|
||||
// getConfigUtil().setConfig(config)
|
||||
// }
|
||||
// }
|
||||
message_data['data']['url'] = IMAGE_HTTP_HOST + url;
|
||||
} else {
|
||||
// 有可能会碰到appid为1406的,这个不能使用新的NT域名,并且需要把appid改为1407才可访问
|
||||
message_data['data']['url'] = `${IMAGE_HTTP_HOST}/download?appid=1407&fileid=${fileUuid}&rkey=${currentRKey}&spec=0`;
|
||||
}
|
||||
} else {
|
||||
message_data['data']['url'] = IMAGE_HTTP_HOST + url;
|
||||
}
|
||||
} else if (fileMd5) {
|
||||
message_data['data']['url'] = `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${fileMd5.toUpperCase()}/0`;
|
||||
}
|
||||
|
||||
if (!message_data['data']['url']) {
|
||||
message_data['data']['url'] = `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${md5HexStr!.toUpperCase()}/0`;
|
||||
}
|
||||
// message_data["data"]["file_id"] = element.picElement.fileUuid
|
||||
message_data['data']['file_size'] = element.picElement.fileSize;
|
||||
dbUtil.addFileCache({
|
||||
name: element.picElement.fileName,
|
||||
path: element.picElement.sourcePath,
|
||||
size: element.picElement.fileSize,
|
||||
url: message_data['data']['url'],
|
||||
uuid: element.picElement.fileUuid || '',
|
||||
msgId: msg.msgId,
|
||||
element: element.picElement,
|
||||
elementType: ElementType.PIC,
|
||||
elementId: element.elementId
|
||||
}).then();
|
||||
// 不自动下载图片
|
||||
|
||||
} else if (element.videoElement || element.fileElement) {
|
||||
const videoOrFileElement = element.videoElement || element.fileElement;
|
||||
const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file;
|
||||
message_data['type'] = ob11MessageDataType;
|
||||
message_data['data']['file'] = videoOrFileElement.fileName;
|
||||
message_data['data']['path'] = videoOrFileElement.filePath;
|
||||
message_data['data']['file_id'] = videoOrFileElement.fileUuid;
|
||||
message_data['data']['file_size'] = videoOrFileElement.fileSize;
|
||||
// 怎么拿到url呢
|
||||
dbUtil.addFileCache({
|
||||
msgId: msg.msgId,
|
||||
name: videoOrFileElement.fileName,
|
||||
path: videoOrFileElement.filePath,
|
||||
size: parseInt(videoOrFileElement.fileSize || '0'),
|
||||
uuid: videoOrFileElement.fileUuid || '',
|
||||
url: '',
|
||||
element: element.videoElement || element.fileElement,
|
||||
elementType: element.videoElement ? ElementType.VIDEO : ElementType.FILE,
|
||||
elementId: element.elementId
|
||||
}).then();
|
||||
} else if (element.pttElement) {
|
||||
message_data['type'] = OB11MessageDataType.voice;
|
||||
message_data['data']['file'] = element.pttElement.fileName;
|
||||
message_data['data']['path'] = element.pttElement.filePath;
|
||||
// message_data["data"]["file_id"] = element.pttElement.fileUuid
|
||||
message_data['data']['file_size'] = element.pttElement.fileSize;
|
||||
dbUtil.addFileCache({
|
||||
name: element.pttElement.fileName,
|
||||
path: element.pttElement.filePath,
|
||||
size: parseInt(element.pttElement.fileSize) || 0,
|
||||
url: '',
|
||||
uuid: element.pttElement.fileUuid || '',
|
||||
msgId: msg.msgId,
|
||||
element: element.pttElement,
|
||||
elementType: ElementType.PTT,
|
||||
elementId: element.elementId
|
||||
}).then();
|
||||
|
||||
// log("收到语音消息", msg)
|
||||
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
|
||||
// console.log("语音转文字结果", text);
|
||||
// }).catch(err => {
|
||||
// console.log("语音转文字失败", err);
|
||||
// })
|
||||
} else if (element.arkElement) {
|
||||
message_data['type'] = OB11MessageDataType.json;
|
||||
message_data['data']['data'] = element.arkElement.bytesData;
|
||||
} else if (element.faceElement) {
|
||||
message_data['type'] = OB11MessageDataType.face;
|
||||
message_data['data']['id'] = element.faceElement.faceIndex.toString();
|
||||
} else if (element.marketFaceElement) {
|
||||
message_data['type'] = OB11MessageDataType.mface;
|
||||
message_data['data']['text'] = element.marketFaceElement.faceName;
|
||||
} else if (element.markdownElement) {
|
||||
message_data['type'] = OB11MessageDataType.markdown;
|
||||
message_data['data']['data'] = element.markdownElement.content;
|
||||
} else if (element.multiForwardMsgElement) {
|
||||
message_data['type'] = OB11MessageDataType.forward;
|
||||
message_data['data']['id'] = msg.msgId;
|
||||
}
|
||||
if (message_data.type !== 'unknown' && message_data.data) {
|
||||
const cqCode = encodeCQCode(message_data);
|
||||
if (messagePostFormat === 'string') {
|
||||
(resMsg.message as string) += cqCode;
|
||||
} else (resMsg.message as OB11MessageData[]).push(message_data);
|
||||
resMsg.raw_message += cqCode;
|
||||
}
|
||||
}
|
||||
resMsg.raw_message = resMsg.raw_message.trim();
|
||||
return resMsg;
|
||||
}
|
||||
|
||||
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | undefined> {
|
||||
if (msg.chatType !== ChatType.group) {
|
||||
return;
|
||||
}
|
||||
if (msg.senderUin) {
|
||||
const member = await getGroupMember(msg.peerUid, msg.senderUin);
|
||||
if (member && member.cardName !== msg.sendMemberName) {
|
||||
const newCardName = msg.sendMemberName || '';
|
||||
const event = new OB11GroupCardEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
|
||||
member.cardName = newCardName;
|
||||
return event;
|
||||
}
|
||||
}
|
||||
// log("group msg", msg);
|
||||
for (const element of msg.elements) {
|
||||
const grayTipElement = element.grayTipElement;
|
||||
const groupElement = grayTipElement?.groupElement;
|
||||
if (groupElement) {
|
||||
// log("收到群提示消息", groupElement)
|
||||
if (groupElement.type == TipGroupElementType.memberIncrease) {
|
||||
log('收到群成员增加消息', groupElement);
|
||||
await sleep(1000);
|
||||
const member = await getGroupMember(msg.peerUid, groupElement.memberUid);
|
||||
const memberUin = member?.uin;
|
||||
// if (!memberUin) {
|
||||
// memberUin = (await NTQQUserApi.getUserDetailInfo(groupElement.memberUid)).uin
|
||||
// }
|
||||
// log("获取新群成员QQ", memberUin)
|
||||
const adminMember = await getGroupMember(msg.peerUid, groupElement.adminUid);
|
||||
// log("获取同意新成员入群的管理员", adminMember)
|
||||
if (memberUin) {
|
||||
const operatorUin = adminMember?.uin || memberUin;
|
||||
const event = new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin));
|
||||
// log("构造群增加事件", event)
|
||||
return event;
|
||||
}
|
||||
} else if (groupElement.type === TipGroupElementType.ban) {
|
||||
log('收到群群员禁言提示', groupElement);
|
||||
const memberUid = groupElement.shutUp!.member.uid;
|
||||
const adminUid = groupElement.shutUp!.admin.uid;
|
||||
let memberUin: string = '';
|
||||
let duration = parseInt(groupElement.shutUp!.duration);
|
||||
const sub_type: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban';
|
||||
// log('OB11被禁言事件', adminUid);
|
||||
if (memberUid) {
|
||||
memberUin = (await getGroupMember(msg.peerUid, memberUid))?.uin || ''; // || (await NTQQUserApi.getUserDetailInfo(memberUid))?.uin
|
||||
} else {
|
||||
memberUin = '0'; // 0表示全员禁言
|
||||
if (duration > 0) {
|
||||
duration = -1;
|
||||
}
|
||||
}
|
||||
const adminUin = (await getGroupMember(msg.peerUid, adminUid))?.uin; // || (await NTQQUserApi.getUserDetailInfo(adminUid))?.uin
|
||||
// log('OB11被禁言事件', memberUin, adminUin, duration, sub_type);
|
||||
if (memberUin && adminUin) {
|
||||
const event = new OB11GroupBanEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, sub_type);
|
||||
return event;
|
||||
}
|
||||
} else if (groupElement.type == TipGroupElementType.kicked) {
|
||||
log('收到我被踢出提示', groupElement);
|
||||
const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin; //|| (await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin
|
||||
if (adminUin) {
|
||||
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), parseInt(adminUin), 'kick_me');
|
||||
}
|
||||
}
|
||||
} else if (element.fileElement) {
|
||||
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin || ''), {
|
||||
id: element.fileElement.fileUuid!,
|
||||
name: element.fileElement.fileName,
|
||||
size: parseInt(element.fileElement.fileSize),
|
||||
busid: element.fileElement.fileBizId || 0
|
||||
});
|
||||
}
|
||||
|
||||
if (grayTipElement) {
|
||||
if (grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER) {
|
||||
log('收到新人被邀请进群消息', grayTipElement);
|
||||
const xmlElement = grayTipElement.xmlElement;
|
||||
if (xmlElement?.content) {
|
||||
const regex = /jp="(\d+)"/g;
|
||||
|
||||
const matches = [];
|
||||
let match = null;
|
||||
|
||||
while ((match = regex.exec(xmlElement.content)) !== null) {
|
||||
matches.push(match[1]);
|
||||
}
|
||||
// log("新人进群匹配到的QQ号", matches)
|
||||
if (matches.length === 2) {
|
||||
const [inviter, invitee] = matches;
|
||||
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), 'invite');
|
||||
}
|
||||
}
|
||||
} else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
|
||||
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
|
||||
/*
|
||||
{
|
||||
align: 'center',
|
||||
items: [
|
||||
{ txt: '恭喜', type: 'nor' },
|
||||
{
|
||||
col: '3',
|
||||
jp: '5',
|
||||
param: ["QQ号"],
|
||||
txt: '林雨辰',
|
||||
type: 'url'
|
||||
},
|
||||
{ txt: '获得群主授予的', type: 'nor' },
|
||||
{
|
||||
col: '3',
|
||||
jp: '',
|
||||
txt: '好好好',
|
||||
type: 'url'
|
||||
},
|
||||
{ txt: '头衔', type: 'nor' }
|
||||
]
|
||||
}
|
||||
|
||||
* */
|
||||
const memberUin = json.items[1].param[0];
|
||||
const title = json.items[3].txt;
|
||||
log('收到群成员新头衔消息', json);
|
||||
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static friend(friend: User): OB11User {
|
||||
return {
|
||||
user_id: parseInt(friend.uin),
|
||||
nickname: friend.nick,
|
||||
remark: friend.remark,
|
||||
sex: OB11Constructor.sex(friend.sex!),
|
||||
level: friend.qqLevel && calcQQLevel(friend.qqLevel) || 0
|
||||
};
|
||||
}
|
||||
|
||||
static selfInfo(selfInfo: SelfInfo): OB11User {
|
||||
return {
|
||||
user_id: parseInt(selfInfo.uin),
|
||||
nickname: selfInfo.nick,
|
||||
};
|
||||
}
|
||||
|
||||
static friends(friends: Friend[]): OB11User[] {
|
||||
const data: OB11User[] = [];
|
||||
friends.forEach(friend => {
|
||||
const sexValue = this.sex(friend.sex!);
|
||||
data.push({ user_id: parseInt(friend.uin), nickname: friend.nick, remark: friend.remark, sex: sexValue, level: 0 });
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
static groupMemberRole(role: number): OB11GroupMemberRole | undefined {
|
||||
return {
|
||||
4: OB11GroupMemberRole.owner,
|
||||
3: OB11GroupMemberRole.admin,
|
||||
2: OB11GroupMemberRole.member
|
||||
}[role];
|
||||
}
|
||||
|
||||
static sex(sex: Sex): OB11UserSex {
|
||||
const sexMap = {
|
||||
[Sex.male]: OB11UserSex.male,
|
||||
[Sex.female]: OB11UserSex.female,
|
||||
[Sex.unknown]: OB11UserSex.unknown
|
||||
};
|
||||
return sexMap[sex] || OB11UserSex.unknown;
|
||||
}
|
||||
|
||||
static groupMember(group_id: string, member: GroupMember): OB11GroupMember {
|
||||
return {
|
||||
group_id: parseInt(group_id),
|
||||
user_id: parseInt(member.uin),
|
||||
nickname: member.nick,
|
||||
card: member.cardName,
|
||||
sex: OB11Constructor.sex(member.sex!),
|
||||
age: 0,
|
||||
area: '',
|
||||
level: 0,
|
||||
qq_level: member.qqLevel && calcQQLevel(member.qqLevel) || 0,
|
||||
join_time: 0, // 暂时没法获取
|
||||
last_sent_time: 0, // 暂时没法获取
|
||||
title_expire_time: 0,
|
||||
unfriendly: false,
|
||||
card_changeable: true,
|
||||
is_robot: member.isRobot,
|
||||
shut_up_timestamp: member.shutUpTime,
|
||||
role: OB11Constructor.groupMemberRole(member.role),
|
||||
title: member.memberSpecialTitle || '',
|
||||
};
|
||||
}
|
||||
|
||||
static stranger(user: User): OB11User {
|
||||
log('construct ob11 stranger', user);
|
||||
return {
|
||||
...user,
|
||||
user_id: parseInt(user.uin),
|
||||
nickname: user.nick,
|
||||
sex: OB11Constructor.sex(user.sex!),
|
||||
age: 0,
|
||||
qid: user.qid,
|
||||
login_days: 0,
|
||||
level: user.qqLevel && calcQQLevel(user.qqLevel) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
static groupMembers(group: Group): OB11GroupMember[] {
|
||||
log('construct ob11 group members', group);
|
||||
return Array.from(groupMembers.get(group.groupCode)?.values() || []).map(m => OB11Constructor.groupMember(group.groupCode, m));
|
||||
}
|
||||
|
||||
static group(group: Group): OB11Group {
|
||||
return {
|
||||
group_id: parseInt(group.groupCode),
|
||||
group_name: group.groupName,
|
||||
member_count: group.memberCount,
|
||||
max_member_count: group.maxMember
|
||||
};
|
||||
}
|
||||
|
||||
static groups(groups: Group[]): OB11Group[] {
|
||||
return groups.map(OB11Constructor.group);
|
||||
}
|
||||
}
|
||||
78
src/onebot11/cqcode.ts
Normal file
78
src/onebot11/cqcode.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { OB11MessageData } from './types';
|
||||
|
||||
const pattern = /\[CQ:(\w+)((,\w+=[^,\]]*)*)\]/;
|
||||
|
||||
function unescape(source: string) {
|
||||
return String(source)
|
||||
.replace(/[/g, '[')
|
||||
.replace(/]/g, ']')
|
||||
.replace(/,/g, ',')
|
||||
.replace(/&/g, '&');
|
||||
}
|
||||
|
||||
function from(source: string) {
|
||||
const capture = pattern.exec(source);
|
||||
if (!capture) return null;
|
||||
const [, type, attrs] = capture;
|
||||
const data: Record<string, any> = {};
|
||||
attrs && attrs.slice(1).split(',').forEach((str) => {
|
||||
const index = str.indexOf('=');
|
||||
data[str.slice(0, index)] = unescape(str.slice(index + 1));
|
||||
});
|
||||
return { type, data, capture };
|
||||
}
|
||||
|
||||
function h(type: string, data: any) {
|
||||
return {
|
||||
type,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeCQCode(source: string): OB11MessageData[] {
|
||||
const elements: any[] = [];
|
||||
let result: ReturnType<typeof from>;
|
||||
while ((result = from(source))) {
|
||||
const { type, data, capture } = result;
|
||||
if (capture.index) {
|
||||
elements.push(h('text', { text: unescape(source.slice(0, capture.index)) }));
|
||||
}
|
||||
elements.push(h(type, data));
|
||||
source = source.slice(capture.index + capture[0].length);
|
||||
}
|
||||
if (source) elements.push(h('text', { text: unescape(source) }));
|
||||
return elements;
|
||||
}
|
||||
|
||||
|
||||
export function encodeCQCode(data: OB11MessageData) {
|
||||
const CQCodeEscapeText = (text: string) => {
|
||||
return text.replace(/&/g, '&')
|
||||
.replace(/\[/g, '[')
|
||||
.replace(/\]/g, ']');
|
||||
|
||||
};
|
||||
|
||||
const CQCodeEscape = (text: string) => {
|
||||
return text.replace(/&/g, '&')
|
||||
.replace(/\[/g, '[')
|
||||
.replace(/\]/g, ']')
|
||||
.replace(/,/g, ',');
|
||||
};
|
||||
|
||||
if (data.type === 'text') {
|
||||
return CQCodeEscapeText(data.data.text);
|
||||
}
|
||||
|
||||
let result = '[CQ:' + data.type;
|
||||
for (const name in data.data) {
|
||||
const value = data.data[name];
|
||||
result += `,${name}=${CQCodeEscape(value)}`;
|
||||
}
|
||||
result += ']';
|
||||
return result;
|
||||
}
|
||||
|
||||
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
|
||||
// const result = parseCQCode("好好好")
|
||||
// console.log(JSON.stringify(result))
|
||||
16
src/onebot11/event/OB11BaseEvent.ts
Normal file
16
src/onebot11/event/OB11BaseEvent.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { selfInfo } from '@/common/data';
|
||||
|
||||
export enum EventType {
|
||||
META = 'meta_event',
|
||||
REQUEST = 'request',
|
||||
NOTICE = 'notice',
|
||||
MESSAGE = 'message',
|
||||
MESSAGE_SENT = 'message_sent',
|
||||
}
|
||||
|
||||
|
||||
export abstract class OB11BaseEvent {
|
||||
time = Math.floor(Date.now() / 1000);
|
||||
self_id = parseInt(selfInfo.uin);
|
||||
post_type: EventType = EventType.META;
|
||||
}
|
||||
5
src/onebot11/event/message/OB11BaseMessageEvent.ts
Normal file
5
src/onebot11/event/message/OB11BaseMessageEvent.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
|
||||
|
||||
export abstract class OB11BaseMessageEvent extends OB11BaseEvent {
|
||||
post_type = EventType.MESSAGE;
|
||||
}
|
||||
6
src/onebot11/event/meta/OB11BaseMetaEvent.ts
Normal file
6
src/onebot11/event/meta/OB11BaseMetaEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
|
||||
|
||||
export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
|
||||
post_type = EventType.META;
|
||||
meta_event_type: string;
|
||||
}
|
||||
21
src/onebot11/event/meta/OB11HeartbeatEvent.ts
Normal file
21
src/onebot11/event/meta/OB11HeartbeatEvent.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { OB11BaseMetaEvent } from './OB11BaseMetaEvent';
|
||||
|
||||
interface HeartbeatStatus {
|
||||
online: boolean | null,
|
||||
good: boolean
|
||||
}
|
||||
|
||||
export class OB11HeartbeatEvent extends OB11BaseMetaEvent {
|
||||
meta_event_type = 'heartbeat';
|
||||
status: HeartbeatStatus;
|
||||
interval: number;
|
||||
|
||||
public constructor(isOnline: boolean, isGood: boolean, interval: number) {
|
||||
super();
|
||||
this.interval = interval;
|
||||
this.status = {
|
||||
online: isOnline,
|
||||
good: isGood
|
||||
};
|
||||
}
|
||||
}
|
||||
17
src/onebot11/event/meta/OB11LifeCycleEvent.ts
Normal file
17
src/onebot11/event/meta/OB11LifeCycleEvent.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { OB11BaseMetaEvent } from './OB11BaseMetaEvent';
|
||||
|
||||
export enum LifeCycleSubType {
|
||||
ENABLE = 'enable',
|
||||
DISABLE = 'disable',
|
||||
CONNECT = 'connect'
|
||||
}
|
||||
|
||||
export class OB11LifeCycleEvent extends OB11BaseMetaEvent {
|
||||
meta_event_type = 'lifecycle';
|
||||
sub_type: LifeCycleSubType;
|
||||
|
||||
public constructor(subType: LifeCycleSubType) {
|
||||
super();
|
||||
this.sub_type = subType;
|
||||
}
|
||||
}
|
||||
5
src/onebot11/event/notice/OB11BaseNoticeEvent.ts
Normal file
5
src/onebot11/event/notice/OB11BaseNoticeEvent.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
|
||||
|
||||
export abstract class OB11BaseNoticeEvent extends OB11BaseEvent {
|
||||
post_type = EventType.NOTICE;
|
||||
}
|
||||
13
src/onebot11/event/notice/OB11FriendRecallNoticeEvent.ts
Normal file
13
src/onebot11/event/notice/OB11FriendRecallNoticeEvent.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
|
||||
|
||||
export class OB11FriendRecallNoticeEvent extends OB11BaseNoticeEvent {
|
||||
notice_type = 'friend_recall';
|
||||
user_id: number;
|
||||
message_id: number;
|
||||
|
||||
public constructor(userId: number, messageId: number) {
|
||||
super();
|
||||
this.user_id = userId;
|
||||
this.message_id = messageId;
|
||||
}
|
||||
}
|
||||
6
src/onebot11/event/notice/OB11GroupAdminNoticeEvent.ts
Normal file
6
src/onebot11/event/notice/OB11GroupAdminNoticeEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_admin';
|
||||
sub_type: 'set' | 'unset'; // "set" | "unset"
|
||||
}
|
||||
17
src/onebot11/event/notice/OB11GroupBanEvent.ts
Normal file
17
src/onebot11/event/notice/OB11GroupBanEvent.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
export class OB11GroupBanEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_ban';
|
||||
operator_id: number;
|
||||
duration: number;
|
||||
sub_type: 'ban' | 'lift_ban';
|
||||
|
||||
constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: 'ban' | 'lift_ban') {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.operator_id = operatorId;
|
||||
this.user_id = userId;
|
||||
this.duration = duration;
|
||||
this.sub_type = sub_type;
|
||||
}
|
||||
}
|
||||
16
src/onebot11/event/notice/OB11GroupCardEvent.ts
Normal file
16
src/onebot11/event/notice/OB11GroupCardEvent.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
export class OB11GroupCardEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_card';
|
||||
card_new: string;
|
||||
card_old: string;
|
||||
|
||||
|
||||
constructor(groupId: number, userId: number, cardNew: string, cardOld: string) {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.user_id = userId;
|
||||
this.card_new = cardNew;
|
||||
this.card_old = cardOld;
|
||||
}
|
||||
}
|
||||
17
src/onebot11/event/notice/OB11GroupDecreaseEvent.ts
Normal file
17
src/onebot11/event/notice/OB11GroupDecreaseEvent.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me';
|
||||
|
||||
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_decrease';
|
||||
sub_type: GroupDecreaseSubType = 'leave'; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
|
||||
operator_id: number;
|
||||
|
||||
constructor(groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.operator_id = operatorId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被踢出的,还是自己主动退出的
|
||||
this.user_id = userId;
|
||||
this.sub_type = subType;
|
||||
}
|
||||
}
|
||||
15
src/onebot11/event/notice/OB11GroupIncreaseEvent.ts
Normal file
15
src/onebot11/event/notice/OB11GroupIncreaseEvent.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
type GroupIncreaseSubType = 'approve' | 'invite';
|
||||
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_increase';
|
||||
operator_id: number;
|
||||
sub_type: GroupIncreaseSubType;
|
||||
constructor(groupId: number, userId: number, operatorId: number, subType: GroupIncreaseSubType = 'approve') {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.operator_id = operatorId;
|
||||
this.user_id = userId;
|
||||
this.sub_type = subType;
|
||||
}
|
||||
}
|
||||
6
src/onebot11/event/notice/OB11GroupNoticeEvent.ts
Normal file
6
src/onebot11/event/notice/OB11GroupNoticeEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
|
||||
|
||||
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
|
||||
group_id: number;
|
||||
user_id: number;
|
||||
}
|
||||
15
src/onebot11/event/notice/OB11GroupRecallNoticeEvent.ts
Normal file
15
src/onebot11/event/notice/OB11GroupRecallNoticeEvent.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_recall';
|
||||
operator_id: number;
|
||||
message_id: number;
|
||||
|
||||
constructor(groupId: number, userId: number, operatorId: number, messageId: number) {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.user_id = userId;
|
||||
this.operator_id = operatorId;
|
||||
this.message_id = messageId;
|
||||
}
|
||||
}
|
||||
15
src/onebot11/event/notice/OB11GroupTitleEvent.ts
Normal file
15
src/onebot11/event/notice/OB11GroupTitleEvent.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
export class OB11GroupTitleEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'notify';
|
||||
sub_type = 'title';
|
||||
title: string;
|
||||
|
||||
|
||||
constructor(groupId: number, userId: number, title: string) {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.user_id = userId;
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
20
src/onebot11/event/notice/OB11GroupUploadNoticeEvent.ts
Normal file
20
src/onebot11/event/notice/OB11GroupUploadNoticeEvent.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
|
||||
|
||||
export interface GroupUploadFile{
|
||||
id: string,
|
||||
name: string,
|
||||
size: number,
|
||||
busid: number,
|
||||
}
|
||||
|
||||
export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent {
|
||||
notice_type = 'group_upload';
|
||||
file: GroupUploadFile;
|
||||
|
||||
constructor(groupId: number, userId: number, file: GroupUploadFile) {
|
||||
super();
|
||||
this.group_id = groupId;
|
||||
this.user_id = userId;
|
||||
this.file = file;
|
||||
}
|
||||
}
|
||||
30
src/onebot11/event/notice/OB11PokeEvent.ts
Normal file
30
src/onebot11/event/notice/OB11PokeEvent.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
|
||||
import { selfInfo } from '../../../common/data';
|
||||
import { OB11BaseEvent } from '../OB11BaseEvent';
|
||||
|
||||
class OB11PokeEvent extends OB11BaseNoticeEvent{
|
||||
notice_type = 'notify';
|
||||
sub_type = 'poke';
|
||||
target_id = parseInt(selfInfo.uin);
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export class OB11FriendPokeEvent extends OB11PokeEvent{
|
||||
sender_id: number;
|
||||
constructor(user_id: number) {
|
||||
super();
|
||||
this.user_id = user_id;
|
||||
this.sender_id = user_id;
|
||||
}
|
||||
}
|
||||
|
||||
export class OB11GroupPokeEvent extends OB11PokeEvent{
|
||||
group_id: number;
|
||||
|
||||
constructor(group_id: number, user_id: number=0) {
|
||||
super();
|
||||
this.group_id = group_id;
|
||||
this.target_id = user_id;
|
||||
this.user_id = user_id;
|
||||
}
|
||||
}
|
||||
11
src/onebot11/event/request/OB11FriendRequest.ts
Normal file
11
src/onebot11/event/request/OB11FriendRequest.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { OB11BaseNoticeEvent } from '../notice/OB11BaseNoticeEvent';
|
||||
import { EventType } from '../OB11BaseEvent';
|
||||
|
||||
|
||||
export class OB11FriendRequestEvent extends OB11BaseNoticeEvent {
|
||||
post_type = EventType.REQUEST;
|
||||
user_id: number = 0;
|
||||
request_type = 'friend' as const;
|
||||
comment: string = '';
|
||||
flag: string = '';
|
||||
}
|
||||
11
src/onebot11/event/request/OB11GroupRequest.ts
Normal file
11
src/onebot11/event/request/OB11GroupRequest.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { OB11GroupNoticeEvent } from '../notice/OB11GroupNoticeEvent';
|
||||
import { EventType } from '../OB11BaseEvent';
|
||||
|
||||
|
||||
export class OB11GroupRequestEvent extends OB11GroupNoticeEvent {
|
||||
post_type = EventType.REQUEST;
|
||||
request_type = 'group' as const;
|
||||
sub_type: 'add' | 'invite' = 'add';
|
||||
comment: string = '';
|
||||
flag: string = '';
|
||||
}
|
||||
99
src/onebot11/index.ts
Normal file
99
src/onebot11/index.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { napCatCore } from '@/core';
|
||||
import { MsgListener } from '@/core/qqnt/listeners';
|
||||
import { NapCatOnebot11 } from '@/onebot11/main';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
import { program } from 'commander';
|
||||
import qrcode from 'qrcode-terminal';
|
||||
import * as readline from 'node:readline';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'node:path';
|
||||
import { noifyLoginStatus } from '@/common/utils/umami';
|
||||
import { checkVersion } from '@/common/utils/version';
|
||||
|
||||
program
|
||||
.option('-q, --qq <type>', 'QQ号')
|
||||
.parse(process.argv);
|
||||
|
||||
const cmdOptions = program.opts();
|
||||
console.log(process.argv);
|
||||
|
||||
checkVersion().then((remoteVersion: string) => {
|
||||
const localVersion = require('./package.json').version;
|
||||
const localVersionList = localVersion.split('.');
|
||||
const remoteVersionList = remoteVersion.split('.');
|
||||
for (const k of [0, 1, 2]) {
|
||||
if (parseInt(remoteVersionList[k]) > parseInt(localVersionList[k])) {
|
||||
console.log('检测到更新,请前往 https://github.com/NapNeko/NapCatQQ 下载 NapCatQQ V', remoteVersion);
|
||||
} else if (parseInt(remoteVersionList[k]) < parseInt(localVersionList[k])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('当前已是最新版本,版本:', localVersion);
|
||||
});
|
||||
new NapCatOnebot11();
|
||||
napCatCore.addLoginSuccessCallback(() => {
|
||||
console.log('login success');
|
||||
noifyLoginStatus();
|
||||
const msgListener = new MsgListener();
|
||||
msgListener.onRecvMsg = (msg) => {
|
||||
// console.log("onRecvMsg", msg)
|
||||
};
|
||||
// napCatCore.getGroupService().getGroupExtList(true).then((res) => {
|
||||
// console.log(res)
|
||||
// })
|
||||
napCatCore.service.msg.addMsgListener(msgListener);
|
||||
});
|
||||
napCatCore.on('system.login.qrcode', (qrCodeData: { url: string, base64: string }) => {
|
||||
console.log('请扫描下面的二维码,然后在手Q上授权登录:');
|
||||
console.log('二维码解码URL:', qrCodeData.url);
|
||||
const qrcodePath = path.join(__dirname, 'qrcode.png');
|
||||
fs.writeFile(qrcodePath, qrCodeData.base64.split('data:image/png;base64')[1], 'base64').then(() => {
|
||||
console.log('二维码已保存到', qrcodePath);
|
||||
});
|
||||
qrcode.generate(qrCodeData.url, {small: true}, (res) => {
|
||||
console.log(res);
|
||||
});
|
||||
});
|
||||
// console.log(cmdOptions);
|
||||
const quickLoginQQ = cmdOptions.qq;
|
||||
napCatCore.on('system.login.error', (result) => {
|
||||
console.error('登录失败', result);
|
||||
napCatCore.login.qrcode().then().catch(console.error);
|
||||
});
|
||||
if (quickLoginQQ) {
|
||||
console.log('quick login', quickLoginQQ);
|
||||
napCatCore.login.quick(quickLoginQQ).then().catch((e) => {
|
||||
console.error(`${quickLoginQQ}快速登录不可用,请检查是否已经登录了`, e);
|
||||
napCatCore.login.qrcode().then();
|
||||
});
|
||||
} else {
|
||||
console.info('没有 -q 参数指定快速登录的QQ,将使用二维码登录方式');
|
||||
napCatCore.login.qrcode().then();
|
||||
}
|
||||
|
||||
// napCatCore.login.service.getLoginList().then((res) => {
|
||||
// const quickLoginUinList = res.LocalLoginInfoList.filter((item) => item.isQuickLogin).map((item) => item.uin);
|
||||
// if (quickLoginUinList.length !== 0) {
|
||||
// const askQuickLoginUin = readline.createInterface({
|
||||
// input: process.stdin,
|
||||
// output: process.stdout
|
||||
// });
|
||||
// const prompt = `选择快速登录的账号\n\n ${quickLoginUinList.map((u, index) => `${index}: ${u}\n`)}\n输入对应序号按回车确定: `;
|
||||
// askQuickLoginUin.question(prompt, (uinIndex) => {
|
||||
// console.log('你选择的是:', uinIndex);
|
||||
// const uin = quickLoginUinList[parseInt(uinIndex)];
|
||||
// if (!uin) {
|
||||
// console.error('请输入正确的序号');
|
||||
// return;
|
||||
// }
|
||||
// console.log('开始登录', uin);
|
||||
// napCatCore.login.quick(uin).then().catch((e) => {
|
||||
// console.error(e);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
//napCatCore.passwordLogin("", "").then(console.log).catch((e) => {
|
||||
// console.log(e)
|
||||
//})
|
||||
335
src/onebot11/main.ts
Normal file
335
src/onebot11/main.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { napCatCore } from '@/core';
|
||||
import { MsgListener } from '@/core/qqnt/listeners';
|
||||
import { OB11Constructor } from '@/onebot11/constructor';
|
||||
import { postOB11Event } from '@/onebot11/server/postOB11Event';
|
||||
import {
|
||||
ChatType,
|
||||
FriendRequest,
|
||||
Group,
|
||||
GroupMemberRole,
|
||||
GroupNotify,
|
||||
GroupNotifyTypes,
|
||||
RawMessage
|
||||
} from '@/core/qqnt/entities';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
import { ob11HTTPServer } from '@/onebot11/server/http';
|
||||
import { ob11WebsocketServer } from '@/onebot11/server/ws/WebsocketServer';
|
||||
import { ob11ReverseWebsockets } from '@/onebot11/server/ws/ReverseWebsocket';
|
||||
import { friendRequests, getFriend, getGroup, getGroupMember, groupNotifies, selfInfo } from '@/common/data';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { BuddyListener, GroupListener, NodeIKernelBuddyListener } from '@/core/qqnt/listeners';
|
||||
import { OB11FriendRequestEvent } from '@/onebot11/event/request/OB11FriendRequest';
|
||||
import { NTQQGroupApi, NTQQUserApi } from '@/core/qqnt/apis';
|
||||
import { log } from '@/common/utils/log';
|
||||
import { OB11GroupRequestEvent } from '@/onebot11/event/request/OB11GroupRequest';
|
||||
import { OB11GroupAdminNoticeEvent } from '@/onebot11/event/notice/OB11GroupAdminNoticeEvent';
|
||||
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '@/onebot11/event/notice/OB11GroupDecreaseEvent';
|
||||
import { OB11FriendRecallNoticeEvent } from '@/onebot11/event/notice/OB11FriendRecallNoticeEvent';
|
||||
import { OB11GroupRecallNoticeEvent } from '@/onebot11/event/notice/OB11GroupRecallNoticeEvent';
|
||||
|
||||
|
||||
export class NapCatOnebot11 {
|
||||
private bootTime: number = Date.now() / 1000;
|
||||
|
||||
|
||||
constructor() {
|
||||
// console.log('ob11 init');
|
||||
napCatCore.addLoginSuccessCallback(this.onReady.bind(this));
|
||||
}
|
||||
|
||||
public onReady() {
|
||||
console.log('ob11 ready');
|
||||
ob11Config.read();
|
||||
if (ob11Config.enableHttp) {
|
||||
ob11HTTPServer.start(ob11Config.httpPort);
|
||||
}
|
||||
if (ob11Config.enableWs) {
|
||||
ob11WebsocketServer.start(ob11Config.wsPort);
|
||||
}
|
||||
if (ob11Config.enableWsReverse) {
|
||||
ob11ReverseWebsockets.start();
|
||||
}
|
||||
// MsgListener
|
||||
const msgListener = new MsgListener();
|
||||
msgListener.onRecvSysMsg = (protobuf: number[]) => {
|
||||
// todo: 解码protobuf,这里可以拿到戳一戳,但是群戳一戳只有群号
|
||||
const buffer = Buffer.from(protobuf);
|
||||
// 转换为十六进制字符串
|
||||
const hexString = protobuf.map(byte => {
|
||||
// 将负数转换为补码表示的正数
|
||||
byte = byte < 0 ? 256 + byte : byte;
|
||||
// 转换为十六进制,确保结果为两位数
|
||||
return ('0' + byte.toString(16)).slice(-2);
|
||||
}).join('');
|
||||
// console.log('ob11 onRecvSysMsg', hexString, Date.now() / 1000);
|
||||
// console.log(buffer.toString());
|
||||
// console.log('ob11 onRecvSysMsg', JSON.stringify(msg, null, 2));
|
||||
};
|
||||
msgListener.onRecvMsg = (msg) => {
|
||||
// console.log('ob11 onRecvMsg', JSON.stringify(msg, null, 2));
|
||||
for (const m of msg) {
|
||||
if (this.bootTime > parseInt(m.msgTime)) {
|
||||
continue;
|
||||
}
|
||||
new Promise((resolve) => {
|
||||
dbUtil.addMsg(m).then(msgShortId => {
|
||||
m.id = msgShortId;
|
||||
this.postReceiveMsg([m]).then().catch(log);
|
||||
}).catch(log);
|
||||
}).then();
|
||||
}
|
||||
};
|
||||
msgListener.onMsgInfoListUpdate = (msgList) => {
|
||||
this.postRecallMsg(msgList).then().catch(log);
|
||||
};
|
||||
msgListener.onAddSendMsg = (msg) => {
|
||||
if (ob11Config.reportSelfMessage) {
|
||||
dbUtil.addMsg(msg).then(id => {
|
||||
msg.id = id;
|
||||
this.postReceiveMsg([msg]).then().catch(log);
|
||||
});
|
||||
}
|
||||
};
|
||||
napCatCore.service.msg.addMsgListener(msgListener);
|
||||
console.log('ob11 msg listener added');
|
||||
|
||||
// BuddyListener
|
||||
const buddyListener = new BuddyListener();
|
||||
buddyListener.onBuddyReqChange = ((req) => {
|
||||
this.postFriendRequest(req.buddyReqs).then().catch(log);
|
||||
});
|
||||
napCatCore.service.buddy.addBuddyListener(buddyListener);
|
||||
console.log('ob11 buddy listener added');
|
||||
|
||||
// GroupListener
|
||||
const groupListener = new GroupListener();
|
||||
groupListener.onGroupNotifiesUpdated = (doubt, notifies) => {
|
||||
// console.log('ob11 onGroupNotifiesUpdated', notifies);
|
||||
this.postGroupNotifies(notifies).then().catch(e => log('postGroupNotifies error: ', e));
|
||||
};
|
||||
groupListener.onJoinGroupNotify = (...notify) => {
|
||||
// console.log('ob11 onJoinGroupNotify', notify);
|
||||
};
|
||||
groupListener.onGroupListUpdate = (updateType, groupList) => {
|
||||
// console.log('ob11 onGroupListUpdate', updateType, groupList);
|
||||
// this.postGroupMemberChange(groupList).then();
|
||||
};
|
||||
|
||||
napCatCore.service.group.addGroupListener(groupListener);
|
||||
console.log('ob11 group listener added');
|
||||
}
|
||||
|
||||
async postReceiveMsg(msgList: RawMessage[]) {
|
||||
const {debug, reportSelfMessage} = ob11Config;
|
||||
for (const message of msgList) {
|
||||
// console.log("ob11 收到新消息", message)
|
||||
// if (message.senderUin !== selfInfo.uin){
|
||||
// message.msgShortId = await dbUtil.addMsg(message);
|
||||
// }
|
||||
|
||||
OB11Constructor.message(message).then((msg) => {
|
||||
if (debug) {
|
||||
msg.raw = message;
|
||||
} else {
|
||||
if (msg.message.length === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const isSelfMsg = msg.user_id.toString() == selfInfo.uin;
|
||||
if (isSelfMsg && !reportSelfMessage) {
|
||||
return;
|
||||
}
|
||||
if (isSelfMsg) {
|
||||
msg.target_id = parseInt(message.peerUin);
|
||||
}
|
||||
postOB11Event(msg);
|
||||
// log("post msg", msg)
|
||||
}).catch(e => log('constructMessage error: ', e));
|
||||
OB11Constructor.GroupEvent(message).then(groupEvent => {
|
||||
if (groupEvent) {
|
||||
// log("post group event", groupEvent);
|
||||
postOB11Event(groupEvent);
|
||||
}
|
||||
}).catch(e => log('constructGroupEvent error: ', e));
|
||||
}
|
||||
}
|
||||
|
||||
async postGroupNotifies(notifies: GroupNotify[]) {
|
||||
for (const notify of notifies) {
|
||||
try {
|
||||
notify.time = Date.now();
|
||||
const notifyTime = parseInt(notify.seq) / 1000 / 1000;
|
||||
// log(`群通知时间${notifyTime}`, `LLOneBot启动时间${this.bootTime}`);
|
||||
if (notifyTime < this.bootTime) {
|
||||
continue;
|
||||
}
|
||||
const flag = notify.group.groupCode + '|' + notify.seq;
|
||||
const existNotify = groupNotifies[flag];
|
||||
if (existNotify) {
|
||||
continue;
|
||||
}
|
||||
log('收到群通知', notify);
|
||||
groupNotifies[flag] = notify;
|
||||
// let member2: GroupMember;
|
||||
// if (notify.user2.uid) {
|
||||
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
|
||||
// }
|
||||
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) {
|
||||
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid);
|
||||
log('有管理员变动通知');
|
||||
// refreshGroupMembers(notify.group.groupCode).then();
|
||||
const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent();
|
||||
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode);
|
||||
log('开始获取变动的管理员');
|
||||
if (member1) {
|
||||
log('变动管理员获取成功');
|
||||
groupAdminNoticeEvent.user_id = parseInt(member1.uin);
|
||||
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? 'unset' : 'set';
|
||||
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
|
||||
postOB11Event(groupAdminNoticeEvent, true);
|
||||
} else {
|
||||
log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode));
|
||||
}
|
||||
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
|
||||
log('有成员退出通知', notify);
|
||||
try {
|
||||
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid);
|
||||
let operatorId = member1.uin;
|
||||
let subType: GroupDecreaseSubType = 'leave';
|
||||
if (notify.user2.uid) {
|
||||
// 是被踢的
|
||||
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid);
|
||||
if (member2) {
|
||||
operatorId = member2.uin;
|
||||
}
|
||||
subType = 'kick';
|
||||
}
|
||||
const groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin), parseInt(operatorId), subType);
|
||||
postOB11Event(groupDecreaseEvent, true);
|
||||
} catch (e: any) {
|
||||
log('获取群通知的成员信息失败', notify, e.stack.toString());
|
||||
}
|
||||
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
|
||||
log('有加群请求');
|
||||
const groupRequestEvent = new OB11GroupRequestEvent();
|
||||
groupRequestEvent.group_id = parseInt(notify.group.groupCode);
|
||||
let requestQQ = '';
|
||||
try {
|
||||
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin;
|
||||
} catch (e) {
|
||||
log('获取加群人QQ号失败', e);
|
||||
}
|
||||
groupRequestEvent.user_id = parseInt(requestQQ) || 0;
|
||||
groupRequestEvent.sub_type = 'add';
|
||||
groupRequestEvent.comment = notify.postscript;
|
||||
groupRequestEvent.flag = flag;
|
||||
postOB11Event(groupRequestEvent);
|
||||
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
|
||||
log('收到邀请我加群通知');
|
||||
const groupInviteEvent = new OB11GroupRequestEvent();
|
||||
groupInviteEvent.group_id = parseInt(notify.group.groupCode);
|
||||
let user_id = (await getFriend(notify.user2.uid))?.uin;
|
||||
if (!user_id) {
|
||||
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin;
|
||||
}
|
||||
groupInviteEvent.user_id = parseInt(user_id);
|
||||
groupInviteEvent.sub_type = 'invite';
|
||||
groupInviteEvent.flag = flag;
|
||||
postOB11Event(groupInviteEvent);
|
||||
}
|
||||
} catch (e: any) {
|
||||
log('解析群通知失败', e.stack.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async postRecallMsg(msgList: RawMessage[]) {
|
||||
for (const message of msgList) {
|
||||
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
|
||||
if (message.recallTime != '0') { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断?
|
||||
// 撤回消息上报
|
||||
const oriMessage = await dbUtil.getMsgByLongId(message.msgId);
|
||||
if (!oriMessage) {
|
||||
continue;
|
||||
}
|
||||
if (message.chatType == ChatType.friend) {
|
||||
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message!.senderUin), oriMessage!.id!);
|
||||
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 || message.senderUin;
|
||||
}
|
||||
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
|
||||
parseInt(message.peerUin),
|
||||
parseInt(message.senderUin),
|
||||
parseInt(operatorId),
|
||||
oriMessage.id!
|
||||
);
|
||||
postOB11Event(groupRecallEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async postFriendRequest(reqs: FriendRequest[]) {
|
||||
for (const req of reqs) {
|
||||
if (parseInt(req.reqTime) < this.bootTime) {
|
||||
continue;
|
||||
}
|
||||
const flag = req.friendUid + '|' + req.reqTime;
|
||||
if (friendRequests[flag]) {
|
||||
continue;
|
||||
}
|
||||
friendRequests[flag] = req;
|
||||
const friendRequestEvent = new OB11FriendRequestEvent();
|
||||
try {
|
||||
const requester = await NTQQUserApi.getUserDetailInfo(req.friendUid);
|
||||
friendRequestEvent.user_id = parseInt(requester.uin);
|
||||
} catch (e) {
|
||||
log('获取加好友者QQ号失败', e);
|
||||
}
|
||||
friendRequestEvent.flag = flag;
|
||||
friendRequestEvent.comment = req.extWords;
|
||||
postOB11Event(friendRequestEvent);
|
||||
}
|
||||
}
|
||||
|
||||
async postGroupMemberChange(groupList: Group[]) {
|
||||
// todo: 有无更好的方法判断群成员变动
|
||||
const newGroupList = groupList;
|
||||
for (const group of newGroupList) {
|
||||
const existGroup = await getGroup(group.groupCode);
|
||||
if (existGroup) {
|
||||
if (existGroup.memberCount > group.memberCount) {
|
||||
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`);
|
||||
const oldMembers = existGroup.members;
|
||||
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode);
|
||||
group.members = newMembers;
|
||||
const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度
|
||||
|
||||
for (const member of newMembers) {
|
||||
newMembersSet.add(member.uin);
|
||||
}
|
||||
|
||||
// 判断bot是否是管理员,如果是管理员不需要从这里得知有人退群,这里的退群无法得知是主动退群还是被踢
|
||||
const bot = await getGroupMember(group.groupCode, selfInfo.uin);
|
||||
if (bot!.role == GroupMemberRole.admin || bot!.role == GroupMemberRole.owner) {
|
||||
continue;
|
||||
}
|
||||
for (const member of oldMembers) {
|
||||
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
|
||||
postOB11Event(new OB11GroupDecreaseEvent(parseInt(group.groupCode), parseInt(member.uin), parseInt(member.uin), 'leave'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// export const napCatOneBot11 = new NapCatOnebot11();
|
||||
17
src/onebot11/onebot11.json
Normal file
17
src/onebot11/onebot11.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"enableHttp": false,
|
||||
"httpPort": 3000,
|
||||
"enableWs": false,
|
||||
"wsPort": 3001,
|
||||
"enableWsReverse": false,
|
||||
"wsReverseUrls": [],
|
||||
"enableHttpPost": false,
|
||||
"httpPostUrls": [],
|
||||
"httpSecret": "",
|
||||
"messagePostFormat": "array",
|
||||
"reportSelfMessage": false,
|
||||
"debug": false,
|
||||
"enableLocalFile2Url": true,
|
||||
"heartInterval": 30000,
|
||||
"token": ""
|
||||
}
|
||||
32
src/onebot11/server/http.ts
Normal file
32
src/onebot11/server/http.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Response } from 'express';
|
||||
import { OB11Response } from '../action/OB11Response';
|
||||
import { HttpServerBase } from '@/common/server/http';
|
||||
import { actionHandlers } from '../action';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
|
||||
class OB11HTTPServer extends HttpServerBase {
|
||||
name = 'OneBot V11 server';
|
||||
|
||||
handleFailed(res: Response, payload: any, e: any) {
|
||||
res.send(OB11Response.error(e.stack.toString(), 200));
|
||||
}
|
||||
|
||||
protected listen(port: number) {
|
||||
if (ob11Config.enableHttp) {
|
||||
super.listen(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ob11HTTPServer = new OB11HTTPServer();
|
||||
|
||||
setTimeout(() => {
|
||||
for (const action of actionHandlers) {
|
||||
for (const method of ['post', 'get']) {
|
||||
ob11HTTPServer.registerRouter(method, action.actionName, (res, payload) => {
|
||||
// @ts-expect-error wait fix
|
||||
return action.handle(payload);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
185
src/onebot11/server/postOB11Event.ts
Normal file
185
src/onebot11/server/postOB11Event.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { OB11Message, OB11MessageAt, OB11MessageData } from '../types';
|
||||
import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent';
|
||||
import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent';
|
||||
import { WebSocket as WebSocketClass } from 'ws';
|
||||
import { wsReply } from './ws/reply';
|
||||
import { log } from '@/common/utils/log';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
import crypto from 'crypto';
|
||||
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/core/qqnt/entities';
|
||||
import { convertMessage2List, createSendElements, sendMsg } from '../action/msg/SendMsg';
|
||||
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest';
|
||||
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
|
||||
import { isNull } from '@/common/utils/helper';
|
||||
import { dbUtil } from '@/common/utils/db';
|
||||
import { friendRequests, getGroup, groupNotifies, selfInfo } from '@/common/data';
|
||||
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/core/qqnt/apis';
|
||||
|
||||
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
|
||||
|
||||
interface QuickActionPrivateMessage {
|
||||
reply?: string;
|
||||
auto_escape?: boolean;
|
||||
}
|
||||
|
||||
interface QuickActionGroupMessage extends QuickActionPrivateMessage {
|
||||
// 回复群消息
|
||||
at_sender?: boolean;
|
||||
delete?: boolean;
|
||||
kick?: boolean;
|
||||
ban?: boolean;
|
||||
ban_duration?: number;
|
||||
//
|
||||
}
|
||||
|
||||
interface QuickActionFriendRequest {
|
||||
approve?: boolean;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
interface QuickActionGroupRequest {
|
||||
approve?: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
type QuickAction =
|
||||
QuickActionPrivateMessage
|
||||
& QuickActionGroupMessage
|
||||
& QuickActionFriendRequest
|
||||
& QuickActionGroupRequest
|
||||
|
||||
const eventWSList: WebSocketClass[] = [];
|
||||
|
||||
export function registerWsEventSender(ws: WebSocketClass) {
|
||||
eventWSList.push(ws);
|
||||
}
|
||||
|
||||
export function unregisterWsEventSender(ws: WebSocketClass) {
|
||||
const index = eventWSList.indexOf(ws);
|
||||
if (index !== -1) {
|
||||
eventWSList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function postWsEvent(event: PostEventType) {
|
||||
for (const ws of eventWSList) {
|
||||
new Promise(() => {
|
||||
wsReply(ws, event);
|
||||
}).then();
|
||||
}
|
||||
}
|
||||
|
||||
export function postOB11Event(msg: PostEventType, reportSelf = false) {
|
||||
const config = ob11Config;
|
||||
// 判断msg是否是event
|
||||
if (!config.reportSelfMessage && !reportSelf) {
|
||||
if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfInfo.uin) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (config.enableHttpPost) {
|
||||
const msgStr = JSON.stringify(msg);
|
||||
const hmac = crypto.createHmac('sha1', ob11Config.httpSecret);
|
||||
hmac.update(msgStr);
|
||||
const sig = hmac.digest('hex');
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-self-id': selfInfo.uin
|
||||
};
|
||||
if (config.httpSecret) {
|
||||
headers['x-signature'] = 'sha1=' + sig;
|
||||
}
|
||||
for (const host of config.httpPostUrls) {
|
||||
fetch(host, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: msgStr
|
||||
}).then(async (res) => {
|
||||
log(`新消息事件HTTP上报成功: ${host} `, msgStr);
|
||||
// todo: 处理不够优雅,应该使用高级泛型进行QuickAction类型识别
|
||||
let resJson: QuickAction;
|
||||
try {
|
||||
resJson = await res.json();
|
||||
log('新消息事件HTTP上报返回快速操作: ', JSON.stringify(resJson));
|
||||
} catch (e) {
|
||||
log('新消息事件HTTP上报没有返回快速操作,不需要处理');
|
||||
return;
|
||||
}
|
||||
if (msg.post_type === 'message') {
|
||||
msg = msg as OB11Message;
|
||||
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id);
|
||||
resJson = resJson as QuickActionPrivateMessage | QuickActionGroupMessage;
|
||||
const reply = resJson.reply;
|
||||
const peer: Peer = {
|
||||
chatType: ChatType.friend,
|
||||
peerUid: msg.user_id.toString()
|
||||
};
|
||||
if (msg.message_type == 'private') {
|
||||
if (msg.sub_type === 'group') {
|
||||
peer.chatType = ChatType.temp;
|
||||
}
|
||||
} else {
|
||||
peer.chatType = ChatType.group;
|
||||
peer.peerUid = msg.group_id!.toString();
|
||||
}
|
||||
if (reply) {
|
||||
let group: Group | undefined;
|
||||
let replyMessage: OB11MessageData[] = [];
|
||||
|
||||
if (msg.message_type == 'group') {
|
||||
group = await getGroup(msg.group_id!.toString());
|
||||
if ((resJson as QuickActionGroupMessage).at_sender) {
|
||||
replyMessage.push({
|
||||
type: 'at',
|
||||
data: {
|
||||
qq: msg.user_id.toString()
|
||||
}
|
||||
} as OB11MessageAt);
|
||||
}
|
||||
}
|
||||
replyMessage = replyMessage.concat(convertMessage2List(reply, resJson.auto_escape));
|
||||
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group);
|
||||
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then();
|
||||
} else if (resJson.delete) {
|
||||
NTQQMsgApi.recallMsg(peer, [rawMessage!.msgId]).then();
|
||||
} else if (resJson.kick) {
|
||||
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage!.senderUid]).then();
|
||||
} else if (resJson.ban) {
|
||||
NTQQGroupApi.banMember(peer.peerUid, [{
|
||||
uid: rawMessage!.senderUid,
|
||||
timeStamp: resJson.ban_duration || 60 * 30
|
||||
}],).then();
|
||||
}
|
||||
|
||||
} else if (msg.post_type === 'request') {
|
||||
if ((msg as OB11FriendRequestEvent).request_type === 'friend') {
|
||||
resJson = resJson as QuickActionFriendRequest;
|
||||
if (!isNull(resJson.approve)) {
|
||||
// todo: set remark
|
||||
const flag = (msg as OB11FriendRequestEvent).flag;
|
||||
// const [friendUid, seq] = flag.split('|');
|
||||
const request = friendRequests[flag];
|
||||
NTQQFriendApi.handleFriendRequest(
|
||||
request,
|
||||
!!resJson.approve,
|
||||
).then();
|
||||
}
|
||||
} else if ((msg as OB11GroupRequestEvent).request_type === 'group') {
|
||||
resJson = resJson as QuickActionGroupRequest;
|
||||
if (!isNull(resJson.approve)) {
|
||||
const flag = (msg as OB11GroupRequestEvent).flag;
|
||||
const request = groupNotifies[flag];
|
||||
// const [groupCode, seq] = flag.split('|');
|
||||
NTQQGroupApi.handleGroupRequest(request,
|
||||
resJson.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject
|
||||
).then();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, (err: any) => {
|
||||
log(`新消息事件HTTP上报失败: ${host} `, err, msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
postWsEvent(msg);
|
||||
}
|
||||
143
src/onebot11/server/ws/ReverseWebsocket.ts
Normal file
143
src/onebot11/server/ws/ReverseWebsocket.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent';
|
||||
import { ActionName } from '../../action/types';
|
||||
import { OB11Response } from '../../action/OB11Response';
|
||||
import BaseAction from '../../action/BaseAction';
|
||||
import { actionMap } from '../../action';
|
||||
import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../postOB11Event';
|
||||
import { wsReply } from './reply';
|
||||
import { WebSocket as WebSocketClass } from 'ws';
|
||||
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent';
|
||||
import { log } from '../../../common/utils/log';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
import { napCatCore } from '@/core';
|
||||
import { selfInfo } from '@/common/data';
|
||||
|
||||
export const rwsList: ReverseWebsocket[] = [];
|
||||
|
||||
export class ReverseWebsocket {
|
||||
public websocket: WebSocketClass | undefined;
|
||||
public url: string;
|
||||
private running: boolean = false;
|
||||
|
||||
public constructor(url: string) {
|
||||
this.url = url;
|
||||
this.running = true;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.running = false;
|
||||
this.websocket!.close();
|
||||
}
|
||||
|
||||
public onopen() {
|
||||
wsReply(this.websocket!, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT));
|
||||
}
|
||||
|
||||
public async onmessage(msg: string) {
|
||||
let receiveData: { action: ActionName | undefined, params: any, echo?: any } = { action: undefined, params: {} };
|
||||
let echo = null;
|
||||
try {
|
||||
receiveData = JSON.parse(msg.toString());
|
||||
echo = receiveData.echo;
|
||||
log('收到反向Websocket消息', receiveData);
|
||||
} catch (e) {
|
||||
return wsReply(this.websocket!, OB11Response.error('json解析失败,请检查数据格式', 1400, echo));
|
||||
}
|
||||
const action: BaseAction<any, any> | undefined = actionMap.get(receiveData.action!);
|
||||
if (!action) {
|
||||
return wsReply(this.websocket!, OB11Response.error('不支持的api ' + receiveData.action, 1404, echo));
|
||||
}
|
||||
try {
|
||||
const handleResult = await action.websocketHandle(receiveData.params, echo);
|
||||
wsReply(this.websocket!, handleResult);
|
||||
} catch (e) {
|
||||
wsReply(this.websocket!, OB11Response.error(`api处理出错:${e}`, 1200, echo));
|
||||
}
|
||||
}
|
||||
|
||||
public onclose = () => {
|
||||
log('反向ws断开', this.url);
|
||||
unregisterWsEventSender(this.websocket!);
|
||||
if (this.running) {
|
||||
this.reconnect();
|
||||
}
|
||||
};
|
||||
|
||||
public send(msg: string) {
|
||||
if (this.websocket && this.websocket.readyState == WebSocket.OPEN) {
|
||||
this.websocket.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private reconnect() {
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, 3000); // TODO: 重连间隔在配置文件中实现
|
||||
}
|
||||
|
||||
private connect() {
|
||||
const { token, heartInterval } = ob11Config;
|
||||
this.websocket = new WebSocketClass(this.url, {
|
||||
handshakeTimeout: 2000,
|
||||
perMessageDeflate: false,
|
||||
headers: {
|
||||
'X-Self-ID': selfInfo.uin,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段
|
||||
}
|
||||
});
|
||||
registerWsEventSender(this.websocket);
|
||||
log('Trying to connect to the websocket server: ' + this.url);
|
||||
|
||||
|
||||
this.websocket.on('open', () => {
|
||||
log('Connected to the websocket server: ' + this.url);
|
||||
this.onopen();
|
||||
});
|
||||
|
||||
this.websocket.on('message', async (data) => {
|
||||
await this.onmessage(data.toString());
|
||||
});
|
||||
|
||||
this.websocket.on('error', log);
|
||||
|
||||
const wsClientInterval = setInterval(() => {
|
||||
postWsEvent(new OB11HeartbeatEvent(!!selfInfo.online, true, heartInterval));
|
||||
}, heartInterval); // 心跳包
|
||||
this.websocket.on('close', () => {
|
||||
clearInterval(wsClientInterval);
|
||||
log('The websocket connection: ' + this.url + ' closed, trying reconnecting...');
|
||||
this.onclose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class OB11ReverseWebsockets {
|
||||
start() {
|
||||
for (const url of ob11Config.wsReverseUrls) {
|
||||
log('开始连接反向ws', url);
|
||||
new Promise(() => {
|
||||
try {
|
||||
rwsList.push(new ReverseWebsocket(url));
|
||||
} catch (e: any) {
|
||||
log(e.stack);
|
||||
}
|
||||
}).then();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
for (const rws of rwsList) {
|
||||
rws.stop();
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.stop();
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
export const ob11ReverseWebsockets = new OB11ReverseWebsockets();
|
||||
|
||||
76
src/onebot11/server/ws/WebsocketServer.ts
Normal file
76
src/onebot11/server/ws/WebsocketServer.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import { actionMap } from '../../action';
|
||||
import { OB11Response } from '../../action/OB11Response';
|
||||
import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../postOB11Event';
|
||||
import { ActionName } from '../../action/types';
|
||||
import BaseAction from '../../action/BaseAction';
|
||||
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent';
|
||||
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent';
|
||||
import { WebsocketServerBase } from '@/common/server/websocket';
|
||||
import { IncomingMessage } from 'node:http';
|
||||
import { wsReply } from './reply';
|
||||
import { napCatCore } from '@/core';
|
||||
import { log } from '../../../common/utils/log';
|
||||
import { ob11Config } from '@/onebot11/config';
|
||||
import { selfInfo } from '@/common/data';
|
||||
|
||||
const heartbeatRunning = false;
|
||||
|
||||
class OB11WebsocketServer extends WebsocketServerBase {
|
||||
authorizeFailed(wsClient: WebSocket) {
|
||||
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
|
||||
}
|
||||
|
||||
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) {
|
||||
const action: BaseAction<any, any> | undefined = actionMap.get(actionName);
|
||||
if (!action) {
|
||||
return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo));
|
||||
}
|
||||
try {
|
||||
const handleResult = await action.websocketHandle(params, echo);
|
||||
wsReply(wsClient, handleResult);
|
||||
} catch (e: any) {
|
||||
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo));
|
||||
}
|
||||
}
|
||||
|
||||
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
|
||||
if (url == '/api' || url == '/api/' || url == '/') {
|
||||
wsClient.on('message', async (msg) => {
|
||||
let receiveData: { action: ActionName, params: any, echo?: any } = { action: '', params: {} };
|
||||
let echo = null;
|
||||
try {
|
||||
receiveData = JSON.parse(msg.toString());
|
||||
echo = receiveData.echo;
|
||||
log('收到正向Websocket消息', receiveData);
|
||||
} catch (e) {
|
||||
return wsReply(wsClient, OB11Response.error('json解析失败,请检查数据格式', 1400, echo));
|
||||
}
|
||||
this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then();
|
||||
});
|
||||
}
|
||||
if (url == '/event' || url == '/event/' || url == '/') {
|
||||
registerWsEventSender(wsClient);
|
||||
|
||||
log('event上报ws客户端已连接');
|
||||
|
||||
try {
|
||||
wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT));
|
||||
} catch (e) {
|
||||
log('发送生命周期失败', e);
|
||||
}
|
||||
const { heartInterval } = ob11Config;
|
||||
const wsClientInterval = setInterval(() => {
|
||||
postWsEvent(new OB11HeartbeatEvent(!!selfInfo.online, true, heartInterval));
|
||||
}, heartInterval); // 心跳包
|
||||
wsClient.on('close', () => {
|
||||
log('event上报ws客户端已断开');
|
||||
clearInterval(wsClientInterval);
|
||||
unregisterWsEventSender(wsClient);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ob11WebsocketServer = new OB11WebsocketServer();
|
||||
|
||||
19
src/onebot11/server/ws/reply.ts
Normal file
19
src/onebot11/server/ws/reply.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { WebSocket as WebSocketClass } from 'ws';
|
||||
import { OB11Response } from '../../action/OB11Response';
|
||||
import { PostEventType } from '../postOB11Event';
|
||||
import { log } from '../../../common/utils/log';
|
||||
import { isNull } from '../../../common/utils/helper';
|
||||
|
||||
|
||||
export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) {
|
||||
try {
|
||||
const packet = Object.assign({}, data);
|
||||
if (isNull(packet['echo'])) {
|
||||
delete packet['echo'];
|
||||
}
|
||||
wsClient.send(JSON.stringify(packet));
|
||||
log('ws 消息上报', wsClient.url || '', data);
|
||||
} catch (e: any) {
|
||||
log('websocket 回复失败', e.stack, data);
|
||||
}
|
||||
}
|
||||
242
src/onebot11/types.ts
Normal file
242
src/onebot11/types.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { PicSubType, RawMessage } from '@/core/entity';
|
||||
import { EventType } from './event/OB11BaseEvent';
|
||||
|
||||
export interface OB11User {
|
||||
user_id: number;
|
||||
nickname: string;
|
||||
remark?: string;
|
||||
sex?: OB11UserSex;
|
||||
level?: number;
|
||||
age?: number;
|
||||
qid?: string;
|
||||
login_days?: number;
|
||||
}
|
||||
|
||||
export enum OB11UserSex {
|
||||
male = 'male',
|
||||
female = 'female',
|
||||
unknown = 'unknown'
|
||||
}
|
||||
|
||||
export enum OB11GroupMemberRole {
|
||||
owner = 'owner',
|
||||
admin = 'admin',
|
||||
member = 'member',
|
||||
}
|
||||
|
||||
export interface OB11GroupMember {
|
||||
group_id: number
|
||||
user_id: number
|
||||
nickname: string
|
||||
card?: string
|
||||
sex?: OB11UserSex
|
||||
age?: number
|
||||
join_time?: number
|
||||
last_sent_time?: number
|
||||
level?: number
|
||||
qq_level?: number
|
||||
role?: OB11GroupMemberRole
|
||||
title?: string
|
||||
area?: string
|
||||
unfriendly?: boolean
|
||||
title_expire_time?: number
|
||||
card_changeable?: boolean
|
||||
// 以下为gocq字段
|
||||
shut_up_timestamp?: number
|
||||
// 以下为扩展字段
|
||||
is_robot?: boolean
|
||||
}
|
||||
|
||||
export interface OB11Group {
|
||||
group_id: number
|
||||
group_name: string
|
||||
member_count?: number
|
||||
max_member_count?: number
|
||||
}
|
||||
|
||||
interface OB11Sender {
|
||||
user_id: number,
|
||||
nickname: string,
|
||||
sex?: OB11UserSex,
|
||||
age?: number,
|
||||
card?: string, // 群名片
|
||||
level?: string, // 群等级
|
||||
role?: OB11GroupMemberRole
|
||||
}
|
||||
|
||||
export enum OB11MessageType {
|
||||
private = 'private',
|
||||
group = 'group'
|
||||
}
|
||||
|
||||
export interface OB11Message {
|
||||
target_id?: number; // 自己发送的消息才有此字段
|
||||
self_id?: number,
|
||||
time: number,
|
||||
message_id: number,
|
||||
real_id: number,
|
||||
user_id: number,
|
||||
group_id?: number,
|
||||
message_type: 'private' | 'group',
|
||||
sub_type?: 'friend' | 'group' | 'normal',
|
||||
sender: OB11Sender,
|
||||
message: OB11MessageData[] | string,
|
||||
message_format: 'array' | 'string',
|
||||
raw_message: string,
|
||||
font: number,
|
||||
post_type?: EventType,
|
||||
raw?: RawMessage
|
||||
}
|
||||
|
||||
export interface OB11ForwardMessage extends OB11Message {
|
||||
content: OB11MessageData[] | string;
|
||||
}
|
||||
|
||||
export interface OB11Return<DataType> {
|
||||
status: string
|
||||
retcode: number
|
||||
data: DataType
|
||||
message: string,
|
||||
echo?: any, // ws调用api才有此字段
|
||||
wording?: string, // go-cqhttp字段,错误信息
|
||||
}
|
||||
|
||||
export enum OB11MessageDataType {
|
||||
text = 'text',
|
||||
image = 'image',
|
||||
music = 'music',
|
||||
video = 'video',
|
||||
voice = 'record',
|
||||
file = 'file',
|
||||
at = 'at',
|
||||
reply = 'reply',
|
||||
json = 'json',
|
||||
face = 'face',
|
||||
mface = 'mface', // 商城表情
|
||||
markdown = 'markdown',
|
||||
node = 'node', // 合并转发消息节点
|
||||
forward = 'forward', // 合并转发消息,用于上报
|
||||
xml = 'xml'
|
||||
}
|
||||
|
||||
export interface OB11MessageMFace {
|
||||
type: OB11MessageDataType.mface,
|
||||
data: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface OB11MessageText {
|
||||
type: OB11MessageDataType.text,
|
||||
data: {
|
||||
text: string, // 纯文本
|
||||
}
|
||||
}
|
||||
|
||||
interface OB11MessageFileBase {
|
||||
data: {
|
||||
thumb?: string;
|
||||
name?: string;
|
||||
file: string,
|
||||
url?: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface OB11MessageImage extends OB11MessageFileBase {
|
||||
type: OB11MessageDataType.image
|
||||
data: OB11MessageFileBase['data'] & {
|
||||
summary?: string; // 图片摘要
|
||||
subType?: PicSubType
|
||||
},
|
||||
}
|
||||
|
||||
export interface OB11MessageRecord extends OB11MessageFileBase {
|
||||
type: OB11MessageDataType.voice
|
||||
}
|
||||
|
||||
export interface OB11MessageFile extends OB11MessageFileBase {
|
||||
type: OB11MessageDataType.file
|
||||
}
|
||||
|
||||
export interface OB11MessageVideo extends OB11MessageFileBase {
|
||||
type: OB11MessageDataType.video
|
||||
}
|
||||
|
||||
export interface OB11MessageAt {
|
||||
type: OB11MessageDataType.at
|
||||
data: {
|
||||
qq: string | 'all'
|
||||
}
|
||||
}
|
||||
|
||||
export interface OB11MessageReply {
|
||||
type: OB11MessageDataType.reply
|
||||
data: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface OB11MessageFace {
|
||||
type: OB11MessageDataType.face
|
||||
data: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export type OB11MessageMixType = OB11MessageData[] | string | OB11MessageData;
|
||||
|
||||
export interface OB11MessageNode {
|
||||
type: OB11MessageDataType.node
|
||||
data: {
|
||||
id?: string
|
||||
user_id?: number
|
||||
nickname: string
|
||||
content: OB11MessageMixType
|
||||
}
|
||||
}
|
||||
|
||||
export interface OB11MessageCustomMusic {
|
||||
type: OB11MessageDataType.music
|
||||
data: {
|
||||
type: 'custom'
|
||||
url: string,
|
||||
audio: string,
|
||||
title: string,
|
||||
content?: string,
|
||||
image?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface OB11MessageJson {
|
||||
type: OB11MessageDataType.json
|
||||
data: { config: { token: string } } & any
|
||||
}
|
||||
|
||||
export type OB11MessageData =
|
||||
OB11MessageText |
|
||||
OB11MessageFace | OB11MessageMFace |
|
||||
OB11MessageAt | OB11MessageReply |
|
||||
OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
|
||||
OB11MessageNode | OB11MessageCustomMusic | OB11MessageJson
|
||||
|
||||
export interface OB11PostSendMsg {
|
||||
message_type?: 'private' | 'group'
|
||||
user_id?: string,
|
||||
group_id?: string,
|
||||
message: OB11MessageMixType;
|
||||
messages?: OB11MessageMixType; // 兼容 go-cqhttp
|
||||
}
|
||||
|
||||
export interface OB11Version {
|
||||
app_name: string
|
||||
app_version: string
|
||||
protocol_version: 'v11'
|
||||
}
|
||||
|
||||
|
||||
export interface OB11Status {
|
||||
online: boolean | null,
|
||||
good: boolean
|
||||
}
|
||||
|
||||
1
src/onebot11/version.ts
Normal file
1
src/onebot11/version.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const version = '0.0.1';
|
||||
Reference in New Issue
Block a user