This commit is contained in:
linyuchen
2024-04-15 00:09:08 +08:00
parent 0ef3e38d70
commit 356aba762c
218 changed files with 8465 additions and 5 deletions

View 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;

View 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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,7 @@
import { GetFileBase } from './GetFile';
import { ActionName } from '../types';
export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage;
}

View 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;
}
}

View 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;
}
}

View 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};
}
}

View 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 };
}
}

View 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));
}
}

View 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;
}

View 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;
}
}

View 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;

View 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;

View 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;

View 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;

View 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;
}
}

View 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;

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}

View 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();

View 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);
}
}

View 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
}
}

View 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;
}
}

View 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;
}
}

View 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;

View 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;

View 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;
}
}

View 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;

View 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;

View 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;
}

View 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
};
}
}

View 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));
});
}

View 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;

View 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
};
}
}

View 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
};
}
}

View 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',
}

View 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;

View 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;
}
}

View 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
View 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
View 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
View File

@@ -0,0 +1,78 @@
import { OB11MessageData } from './types';
const pattern = /\[CQ:(\w+)((,\w+=[^,\]]*)*)\]/;
function unescape(source: string) {
return String(source)
.replace(/&#91;/g, '[')
.replace(/&#93;/g, ']')
.replace(/&#44;/g, ',')
.replace(/&amp;/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, '&amp;')
.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;');
};
const CQCodeEscape = (text: string) => {
return text.replace(/&/g, '&amp;')
.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;')
.replace(/,/g, '&#44;');
};
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))

View 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;
}

View File

@@ -0,0 +1,5 @@
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
export abstract class OB11BaseMessageEvent extends OB11BaseEvent {
post_type = EventType.MESSAGE;
}

View File

@@ -0,0 +1,6 @@
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
post_type = EventType.META;
meta_event_type: string;
}

View 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
};
}
}

View 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;
}
}

View File

@@ -0,0 +1,5 @@
import { EventType, OB11BaseEvent } from '../OB11BaseEvent';
export abstract class OB11BaseNoticeEvent extends OB11BaseEvent {
post_type = EventType.NOTICE;
}

View 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;
}
}

View File

@@ -0,0 +1,6 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_admin';
sub_type: 'set' | 'unset'; // "set" | "unset"
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,6 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number;
user_id: number;
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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 = '';
}

View 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
View 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
View 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();

View 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": ""
}

View 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);

View 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);
}

View 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();

View 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();

View 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
View 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
View File

@@ -0,0 +1 @@
export const version = '0.0.1';