-
Notifications
You must be signed in to change notification settings - Fork 73
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
558 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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('不支持此元素'); | ||
} | ||
}; |
Oops, something went wrong.