Skip to content

Commit

Permalink
feat: NapCat 类型
Browse files Browse the repository at this point in the history
  • Loading branch information
clansty committed Jul 11, 2024
1 parent a56dbc3 commit ec4a69f
Show file tree
Hide file tree
Showing 14 changed files with 558 additions and 29 deletions.
1 change: 1 addition & 0 deletions main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@types/node": "^20.14.10",
"@types/probe-image-size": "^7.2.4",
"@types/prompts": "^2.4.9",
"node-napcat-ts": "^0.1.2",
"tsx": "^4.16.2"
},
"dependencies": {
Expand Down
109 changes: 109 additions & 0 deletions main/src/client/NapCatClient/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { CreateQQClientParamsBase, Friend, Group, QQClient } from '../QQClient';
import random from '../../utils/random';
import { getLogger, Logger } from 'log4js';
import posthog from '../../models/posthog';
import type { WSSendParam, WSSendReturn } from 'node-napcat-ts';
import { NapCatFriend, NapCatGroup } from './entity';

export interface CreateNapCatParams extends CreateQQClientParamsBase {
type: 'napcat';
wsUrl: string;
}

export class NapCatClient extends QQClient {
private constructor(id: number, private readonly wsUrl: string) {
super(id);
this.logger = getLogger(`NapCatClient - ${id}`);
this.ws = new WebSocket(wsUrl);
this.ws.onmessage = (e) => this.handleWebSocketMessage(e.data);
}

private readonly ws: WebSocket;
private readonly logger: Logger;

public static async create(params: CreateNapCatParams) {
const instance = new this(params.id, params.wsUrl);
return new Promise<NapCatClient>((resolve, reject) => {
instance.ws.onopen = async () => {
instance.logger.info('WS 连接成功');
instance.ws.onerror = null;
await instance.refreshSelf();
resolve(instance);
};
instance.ws.onerror = (e) => {
instance.logger.error('WS 连接出错', e);
posthog.capture('WS 连接出错', { error: e });
reject(e);
};
});
}

private readonly echoMap: { [key: string]: { resolve: (result: any) => void; reject: (result: any) => void } } = {};

public async callApi<T extends keyof WSSendReturn>(action: T, params?: WSSendParam[T]): Promise<WSSendReturn[T]> {
return new Promise<WSSendReturn[T]>((resolve, reject) => {
const echo = `${new Date().getTime()}${random.int(100000, 999999)}`;
this.echoMap[echo] = { resolve, reject };
this.ws.send(JSON.stringify({ action, params, echo }));
this.logger.trace('send', JSON.stringify({ action, params, echo }));
});
}

private async handleWebSocketMessage(message: string) {
this.logger.trace('receive', message);
const data = JSON.parse(message);
if (data.echo) {
const promise = this.echoMap[data.echo];
if (!promise) return;
if (data.status === 'ok') {
promise.resolve(data.data);
}
else {
promise.reject(data.message);
}
return;
}
}

public uin: number;
public nickname: string;

public async refreshSelf() {
const data = await this.callApi('get_login_info');
this.uin = data.user_id;
this.nickname = data.nickname;
}

public async isOnline(): Promise<boolean> {
const data = await this.callApi('get_status');
return data.online;
}

public async getFriendsWithCluster(): Promise<{ name: string; friends: Friend[]; }[]> {
const data = await this.callApi('get_friends_with_category');
return data.map(it => ({
name: it.categoryName,
friends: it.buddyList.map(friend => NapCatFriend.createExisted(this, {
nickname: friend.nick,
uid: parseInt(friend.uin),
remark: friend.remark,
})),
}));
}

public pickFriend(uin: number): Promise<Friend> {
return NapCatFriend.create(this, uin);
}

public async getGroupList(): Promise<Group[]> {
const data = await this.callApi('get_group_list');
return data.map(it => NapCatGroup.createExisted(this, {
gid: it.group_id,
name: it.group_name,
}));
}

public pickGroup(groupId: number): Promise<Group> {
return NapCatGroup.create(this, groupId);
}
}
147 changes: 147 additions & 0 deletions main/src/client/NapCatClient/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { Receive, Send } from 'node-napcat-ts';
import { SendableElem } from '../QQClient';
import { MessageElem, segment } from '@icqqjs/icqq';

export const messageElemToNapCatSendable = (elem: SendableElem): Send[keyof Send] => {
switch (elem.type) {
case 'at':
return {
type: elem.type,
data: elem,
};
case 'text':
return {
type: elem.type,
data: elem,
};
case 'face':
return {
type: elem.type,
data: elem,
};
case 'rps':
case 'dice':
return {
type: elem.type,
data: {
result: elem.id,
},
};
// TODO: 文件落本地
case 'image':
if (elem.file !== 'string') {
throw new Error('TODO');
}
return {
type: elem.type,
data: {
file: elem.file,
summary: '图片',
name: '图片',
},
};
case 'record':
if (elem.file !== 'string') {
throw new Error('TODO');
}
return {
type: elem.type,
data: {
file: elem.file,
name: '语音',
},
};
case 'video':
if (elem.file !== 'string') {
throw new Error('TODO');
}
return {
type: elem.type,
data: {
file: elem.file,
name: '视频',
},
};
case 'sface':
default:
throw new Error('不支持此元素');
}
};

export const napCatReceiveToMessageElem = (data: Receive[keyof Receive]): MessageElem | Receive['forward'] => {
switch (data.type) {
case 'text':
return {
...data.data,
type: data.type,
};
case 'face':
return {
...data.data,
type: data.type,
};
case 'mface':
return {
type: 'image',
url: data.data.url,
file: data.data.url,
};
case 'at':
return {
...data.data,
type: data.type,
};
case 'image':
return {
...data.data,
type: data.type,
};
case 'record':
return {
...data.data,
type: data.type,
};
case 'file':
return {
...data.data,
type: 'file',
duration: 0,
name: data.data.file,
fid: data.data.file_id,
size: data.data.file_size,
md5: '',
};
case 'video':
return {
type: data.type,
// 我们不需要 fileId,直接能拿到 url,url 进 getVideoUrl 转一圈拿回来自己,保持兼容性
fid: data.data.url,
file: data.data.url,
};
case 'json':
return {
...data.data,
type: data.type,
};
case 'dice':
case 'rps':
return {
id: data.data.result,
type: data.type,
};
case 'markdown':
return {
...data.data,
type: data.type,
};
case 'forward':
return data;
case 'reply':
throw new Error('不出意外这个应该提前处理');
case 'music':
case 'customMusic':
throw new Error('这个真的能被收到吗');
default:
throw new Error('不支持此元素');
}
};
Loading

0 comments on commit ec4a69f

Please sign in to comment.