197 lines
6.4 KiB
TypeScript
197 lines
6.4 KiB
TypeScript
import MiraiTs, { MiraiApiHttpConfig } from 'mirai-ts'
|
|
import { Mutex } from 'async-mutex'
|
|
import yargs from 'yargs'
|
|
import { readFileSync } from 'fs'
|
|
|
|
const args = yargs.option('config', {
|
|
alias: 'c',
|
|
description: 'path to the config json',
|
|
type: 'string'
|
|
}).help().alias('help', 'h').argv
|
|
|
|
type ExpectedType<T extends (arg: unknown) => unknown> =
|
|
T extends (arg: unknown) => arg is infer R ? R : never
|
|
type Definition = Record<string, (arg: unknown) => unknown>
|
|
export type FromDefinition<D extends Definition> = {
|
|
[K in keyof D]: ExpectedType<D[K]>
|
|
}
|
|
export const getTypeChecker = <D extends Definition>(d: D) => {
|
|
const keys = Object.keys(d) as (keyof Definition)[]
|
|
return (e: unknown): e is FromDefinition<D> => {
|
|
return typeof e === 'object' && keys.every(k => {
|
|
return d[k]((e as any)[k])
|
|
})
|
|
}
|
|
}
|
|
const configDefinition = {
|
|
qq: (x: unknown): x is number => typeof x === 'number',
|
|
mahConfig: (x: unknown): x is MiraiApiHttpConfig => typeof x === 'object',
|
|
groups: (x: unknown): x is number[] => Array.isArray(x) && x.every(e => typeof e === 'number')
|
|
}
|
|
const parseConfig = () => {
|
|
if (typeof args.config !== 'string') {
|
|
throw new Error('missing config path')
|
|
}
|
|
const parsed = JSON.parse(readFileSync(args.config).toString('utf-8'))
|
|
const configChecker = getTypeChecker(configDefinition)
|
|
if (!configChecker(parsed)) {
|
|
throw new Error('Invalid config')
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
const { qq, mahConfig, groups: groupNumbers } = parseConfig();
|
|
const mirai = new MiraiTs(mahConfig)
|
|
|
|
type StoredMessage = {
|
|
id: number
|
|
author: number
|
|
}
|
|
|
|
class StoredMessages {
|
|
private threshold = 200
|
|
private data = [new Map<number, StoredMessage>(), new Map<number, StoredMessage>()]
|
|
private currentIndex = 0
|
|
private otherIndex = 1
|
|
private get current() { return this.data[this.currentIndex] }
|
|
private get other() { return this.data[this.otherIndex] }
|
|
|
|
public add(original: number, translated: StoredMessage) {
|
|
if (this.current.size > this.threshold) {
|
|
// 假如保存的消息过多,那么把另一个 buffer (老数据)给清空后拿来使用
|
|
[this.currentIndex, this.otherIndex] = [this.otherIndex, this.currentIndex]
|
|
this.current.clear()
|
|
}
|
|
const current = this.current
|
|
current.set(original, translated)
|
|
}
|
|
|
|
public translate(messageId: number) {
|
|
return this.current.get(messageId) || this.other.get(messageId)
|
|
}
|
|
}
|
|
|
|
async function app() {
|
|
// 登录 QQ
|
|
await mirai.link(qq);
|
|
|
|
// 对收到的消息进行处理
|
|
// message 本质相当于同时绑定了 FriendMessage GroupMessage TempMessage
|
|
// 你也可以单独对某一类消息进行监听
|
|
const groups = groupNumbers.map(group => ({
|
|
group,
|
|
stored: new StoredMessages()
|
|
}));
|
|
const mutex = new Mutex()
|
|
mirai.on('GroupMessage', async (msg) => {
|
|
const fromGroup = msg.sender.group.id
|
|
if (!groups.find(({ group }) => group === fromGroup)) {
|
|
return
|
|
}
|
|
const messageId = msg.messageChain[0].id
|
|
const messageAuthor = msg.sender.id
|
|
const originalGroupStorage = groups
|
|
.find(({ group }) => group === fromGroup)
|
|
?.stored;
|
|
|
|
const releaseMutex = await mutex.acquire()
|
|
try {
|
|
const promises = groups
|
|
.filter(({ group }) => group !== fromGroup)
|
|
.map(async ({ group, stored }) => {
|
|
let quote: StoredMessage | undefined
|
|
let atMeCounter = 0
|
|
const processed = msg.messageChain.filter(x => {
|
|
// 处理 @
|
|
if (x.type === 'At') {
|
|
// 避免转发回复时的 @
|
|
if (quote && x.target === qq) {
|
|
return atMeCounter++ === 0
|
|
}
|
|
return true
|
|
}
|
|
// 处理回复
|
|
if (x.type !== 'Quote') {
|
|
return true
|
|
}
|
|
quote = stored.translate(x.id) || quote
|
|
return false
|
|
}).map(x => {
|
|
// 处理 @
|
|
if (x.type !== 'At') {
|
|
return x
|
|
}
|
|
|
|
if (x.target === qq) {
|
|
x = { ...x, target: quote?.author || qq }
|
|
}
|
|
/*
|
|
// 处理转发之后 At 可能由于对象不在转发的群里而出现的问题
|
|
const plain: Plain = { type: 'Plain', text: `@${x.display}` }
|
|
try {
|
|
mirai.api.memberList
|
|
const info: MemberInfo = await mirai.api.memberInfo(group, x.target, undefined as unknown as MemberInfo)
|
|
if (typeof info.name !== 'string') {
|
|
return plain
|
|
}
|
|
}
|
|
catch {
|
|
return plain
|
|
}
|
|
*/
|
|
return x
|
|
})
|
|
|
|
try {
|
|
const chain = await Promise.all(processed)
|
|
const sent = await mirai.api.sendGroupMessage(chain, group, quote?.id)
|
|
return { sentId: sent.messageId, targetStorage: stored }
|
|
}
|
|
catch (e) {
|
|
console.warn(e)
|
|
}
|
|
})
|
|
|
|
const results = (await Promise.all(promises))
|
|
.filter(<T>(x?: T): x is T => x !== undefined)
|
|
for (const { sentId, targetStorage } of results) {
|
|
const others = results.filter(other => other.targetStorage !== targetStorage)
|
|
for (const other of others) {
|
|
targetStorage.add(other.sentId, { author: messageAuthor, id: sentId })
|
|
}
|
|
targetStorage.add(messageId, { author: messageAuthor, id: sentId })
|
|
originalGroupStorage?.add(sentId, { author: messageAuthor, id: messageId })
|
|
}
|
|
}
|
|
finally {
|
|
releaseMutex();
|
|
}
|
|
})
|
|
|
|
// 调用 mirai-ts 封装的 mirai-api-http 发送指令
|
|
/*console.log("send command help");
|
|
const data = await mirai.api.command.send("help", []);
|
|
console.log("帮助信息:" + data);*/
|
|
|
|
// 处理各种事件类型
|
|
// 事件订阅说明(名称均与 mirai-api-http 中事件名一致)
|
|
// https://github.com/RedBeanN/node-mirai/blob/master/event.md
|
|
// console.log("on other event");
|
|
// https://github.com/project-mirai/mirai-api-http/blob/master/EventType.md#群消息撤回
|
|
mirai.on("GroupRecallEvent", ({ operator }) => {
|
|
if (operator) {
|
|
const text = `${operator.memberName} 撤回了一条消息,并拜托你不要再发色图了。`;
|
|
console.log(text);
|
|
mirai.api.sendGroupMessage(text, operator.group.id);
|
|
}
|
|
});
|
|
|
|
// 开始监听
|
|
mirai.listen();
|
|
// 可传入回调函数对监听的函数进行处理,如:
|
|
// mirai.listen((msg) => {
|
|
// console.log(msg)
|
|
// })
|
|
}
|
|
|
|
app(); |