From 07e6194eda59af13d6f684869becdc2829357175 Mon Sep 17 00:00:00 2001 From: Clansty Date: Wed, 10 Jul 2024 23:45:02 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=A7=A3=E8=80=A6=E5=90=88=20icqq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/package.json | 1 + main/src/api/richHeader.ts | 27 +- main/src/client/OicqClient.ts | 237 ++++++++--- main/src/client/QQClient/entity.ts | 66 ++++ main/src/client/QQClient/events.ts | 113 ++++++ main/src/client/QQClient/index.ts | 191 +++++++++ main/src/constants/commands.ts | 4 - main/src/controllers/AliveCheckController.ts | 11 +- main/src/controllers/ConfigController.ts | 54 +-- .../controllers/DeleteMessageController.ts | 12 +- .../FileAndFlashPhotoController.ts | 7 +- main/src/controllers/ForwardController.ts | 95 ++--- main/src/controllers/HugController.ts | 37 +- .../controllers/InChatCommandsController.ts | 21 +- .../controllers/MiraiSkipFilterController.ts | 8 +- .../controllers/OicqErrorNotifyController.ts | 12 +- main/src/controllers/QuotLyController.ts | 26 +- main/src/controllers/RequestController.ts | 8 +- main/src/controllers/SetupController.ts | 11 +- .../src/controllers/StatusReportController.ts | 32 -- main/src/helpers/RecoverMessageHelper.ts | 370 ------------------ main/src/index.ts | 1 + main/src/models/ForwardPairs.ts | 21 +- main/src/models/Instance.ts | 11 +- main/src/models/Pair.ts | 4 +- main/src/services/ConfigService.ts | 70 ++-- main/src/services/DeleteMessageService.ts | 6 +- main/src/services/ForwardService.ts | 100 ++--- main/src/services/InChatCommandsService.ts | 30 +- main/src/services/SetupService.ts | 6 +- main/src/utils/getAboutText.ts | 10 +- main/src/utils/urls.ts | 12 +- pnpm-lock.yaml | 58 +-- 33 files changed, 879 insertions(+), 793 deletions(-) create mode 100644 main/src/client/QQClient/entity.ts create mode 100644 main/src/client/QQClient/events.ts create mode 100644 main/src/client/QQClient/index.ts delete mode 100644 main/src/controllers/StatusReportController.ts delete mode 100644 main/src/helpers/RecoverMessageHelper.ts diff --git a/main/package.json b/main/package.json index ca1c94c4..07d1286c 100644 --- a/main/package.json +++ b/main/package.json @@ -47,6 +47,7 @@ "log4js": "^6.9.1", "markdown-escape": "^2.0.0", "nodejs-base64": "^2.0.0", + "posthog-node": "^4.0.1", "prisma": "5.16.2", "probe-image-size": "^7.2.3", "prompts": "^2.4.2", diff --git a/main/src/api/richHeader.ts b/main/src/api/richHeader.ts index bd42a1c5..9f80129d 100644 --- a/main/src/api/richHeader.ts +++ b/main/src/api/richHeader.ts @@ -2,8 +2,9 @@ import { FastifyPluginCallback } from 'fastify'; import { Pair } from '../models/Pair'; import ejs from 'ejs'; import fs from 'fs'; -import { Group } from '@icqqjs/icqq'; +import { Group as OicqGroup, Member as OicqMember } from '@icqqjs/icqq'; import { format } from 'date-and-time'; +import { Group, GroupMemberInfo } from '../client/QQClient'; const template = ejs.compile(fs.readFileSync('./assets/richHeader.ejs', 'utf-8')); @@ -17,25 +18,31 @@ export default ((fastify, opts, done) => { return 'Group not found'; } const group = pair.qq as Group; - const members = await group.getMemberMap(); - const member = members.get(Number(request.params.userId)); + const member = group.pickMember(Number(request.params.userId), true); if (!member) { reply.code(404); return 'Member not found'; } - const profile = await pair.qq.client.getProfile(member.user_id); + const profile = group instanceof OicqGroup ? await group.client.getProfile(member.uid) : ({} as any); // TODO + let memberInfo: GroupMemberInfo; + if (member instanceof OicqMember) { + memberInfo = member.info; + } + if (!memberInfo) { + memberInfo = {} as any; + } reply.type('text/html'); return template({ userId: request.params.userId, - title: member.title, - name: member.card || member.nickname, - role: member.role, - joinTime: format(new Date(member.join_time * 1000), 'YYYY-MM-DD HH:mm'), - lastSentTime: format(new Date(member.last_sent_time * 1000), 'YYYY-MM-DD HH:mm'), + title: memberInfo.title, + name: memberInfo.card || memberInfo.nickname, + role: memberInfo.role, + joinTime: format(new Date(memberInfo.join_time * 1000), 'YYYY-MM-DD HH:mm'), + lastSentTime: format(new Date(memberInfo.last_sent_time * 1000), 'YYYY-MM-DD HH:mm'), regTime: format(new Date(profile.regTimestamp * 1000), 'YYYY-MM-DD HH:mm'), location: [profile.country, profile.province, profile.city].join(' ').trim(), - nickname: member.nickname, + nickname: memberInfo.nickname, email: profile.email, qid: profile.QID, signature: profile.signature, diff --git a/main/src/client/OicqClient.ts b/main/src/client/OicqClient.ts index e5105711..eebed0d1 100644 --- a/main/src/client/OicqClient.ts +++ b/main/src/client/OicqClient.ts @@ -1,11 +1,18 @@ import { Client, - DiscussMessageEvent, Forwardable, - Friend, - Group, + DiscussMessageEvent, + Forwardable, GroupMessageEvent, - LogLevel, Platform, PrivateMessage, + LogLevel, + MemberDecreaseEvent, + MemberIncreaseEvent, + Platform, + PrivateMessage, PrivateMessageEvent, + FriendIncreaseEvent as OicqFriendIncreaseEvent, + FriendRecallEvent, + GroupRecallEvent, + FriendPokeEvent, GroupPokeEvent, MessageElem, FriendRequestEvent, GroupInviteEvent, type ImageElem, XmlElem, } from '@icqqjs/icqq'; import random from '../utils/random'; import fs from 'fs'; @@ -15,16 +22,22 @@ import dataPath from '../helpers/dataPath'; import os from 'os'; import { Converter, Image, rand2uuid } from '@icqqjs/icqq/lib/message'; import { randomBytes } from 'crypto'; -import { gzip, timestamp } from '@icqqjs/icqq/lib/common'; +import { escapeXml, gzip, timestamp } from '@icqqjs/icqq/lib/common'; import { pb } from '@icqqjs/icqq/lib/core'; import env from '../models/env'; +import { + CreateQQClientParamsBase, Friend, FriendIncreaseEvent, + GroupMemberDecreaseEvent, + GroupMemberIncreaseEvent, + MessageEvent, MessageRecallEvent, PokeEvent, + QQClient, +} from './QQClient'; const LOG_LEVEL: LogLevel = env.OICQ_LOG_LEVEL; -type MessageHandler = (event: PrivateMessageEvent | GroupMessageEvent) => Promise -interface CreateOicqParams { - id: number; +export interface CreateOicqParams extends CreateQQClientParamsBase { + type: 'oicq'; uin: number; password: string; platform: Platform; @@ -38,41 +51,49 @@ interface CreateOicqParams { } // OicqExtended?? -export default class OicqClient extends Client { - private readonly onMessageHandlers: Array = []; +export default class OicqClient extends QQClient { + public readonly oicq: Client; - private constructor(uin: number, public readonly id: number, conf?: Config, + private constructor(uin: number, id: number, conf?: Config, public readonly signDockerId?: string) { - super(conf); + super(id); + this.oicq = new Client(conf); + } + + public get uin() { + return this.oicq.uin; } - private static existedBots = {} as { [id: number]: OicqClient }; + public get nickname() { + return this.oicq.nickname; + } + + public isOnline() { + return this.oicq.isOnline(); + } private isOnMessageCreated = false; public static create(params: CreateOicqParams) { - if (this.existedBots[params.id]) { - return Promise.resolve(this.existedBots[params.id]); - } return new Promise(async (resolve, reject) => { const loginDeviceHandler = async ({ phone }: { url: string, phone: string }) => { - client.sendSmsCode(); + await client.oicq.sendSmsCode(); const code = await params.onVerifyDevice(phone); if (code === 'qrsubmit') { - client.login(); + await client.oicq.login(); } else { - client.submitSmsCode(code); + await client.oicq.submitSmsCode(code); } }; const loginSliderHandler = async ({ url }: { url: string }) => { const res = await params.onVerifySlider(url); if (res) { - client.submitSlider(res); + client.oicq.submitSlider(res); } else { - client.login(); + client.oicq.login(); } }; @@ -81,13 +102,22 @@ export default class OicqClient extends Client { }; const successLoginHandler = () => { - client.offTrap('system.login.device', loginDeviceHandler); - client.offTrap('system.login.slider', loginSliderHandler); - client.offTrap('system.login.error', loginErrorHandler); - client.offTrap('system.online', successLoginHandler); + client.oicq.offTrap('system.login.device', loginDeviceHandler); + client.oicq.offTrap('system.login.slider', loginSliderHandler); + client.oicq.offTrap('system.login.error', loginErrorHandler); + client.oicq.offTrap('system.online', successLoginHandler); if (!client.isOnMessageCreated) { - client.trap('message', client.onMessage); + client.oicq.trap('message', client.onMessage); + client.oicq.trap('notice.group.decrease', client.onGroupMemberDecrease); + client.oicq.trap('notice.group.increase', client.onGroupMemberIncrease); + client.oicq.trap('notice.friend.increase', client.onFriendIncrease); + client.oicq.trap('notice.friend.recall', client.onMessageRecall); + client.oicq.trap('notice.group.recall', client.onMessageRecall); + client.oicq.trap('notice.friend.poke', client.onPoke); + client.oicq.trap('notice.group.poke', client.onPoke); + client.oicq.trap('request.friend', client.onFriendRequest); + client.oicq.trap('request.group.invite', client.onGroupInvite); client.isOnMessageCreated = true; } @@ -126,41 +156,92 @@ export default class OicqClient extends Client { sign_api_addr: params.signApi || env.SIGN_API, ver: params.signVer || env.SIGN_VER, }, params.signDockerId); - client.on('system.login.device', loginDeviceHandler); - client.on('system.login.slider', loginSliderHandler); - client.on('system.login.error', loginErrorHandler); - client.on('system.online', successLoginHandler); + client.oicq.on('system.login.device', loginDeviceHandler); + client.oicq.on('system.login.slider', loginSliderHandler); + client.oicq.on('system.login.error', loginErrorHandler); + client.oicq.on('system.online', successLoginHandler); - this.existedBots[params.id] = client; - client.login(params.uin, params.password); + client.oicq.login(params.uin, params.password); }); } private onMessage = async (event: PrivateMessageEvent | GroupMessageEvent | DiscussMessageEvent) => { if (event.message_type === 'discuss') return; + + const gEvent = new MessageEvent( + { id: event.sender.user_id, name: ('card' in event.sender && event.sender.card) || event.sender.nickname }, + 'group' in event ? event.group : event.friend, + event.message, + event.seq, + event.rand, + event.pktnum, + event.time, + event.raw_message, + event.source && { ...event.source, fromId: event.source.user_id, message: event.source.message as MessageElem[] }, + 'anonymous' in event ? event.anonymous : undefined, + event.message_id, + 'atme' in event ? event.atme : false, + 'atall' in event ? event.atall : false, + ); for (const handler of this.onMessageHandlers) { - const res = await handler(event); + const res = await handler(gEvent); if (res) return; } }; - public addNewMessageEventHandler(handler: MessageHandler) { - this.onMessageHandlers.push(handler); - } + private onGroupMemberIncrease = async (event: MemberIncreaseEvent) => { + const gEvent = new GroupMemberIncreaseEvent(event.group, event.user_id, event.nickname); + for (const handler of this.onGroupMemberIncreaseHandlers) { + const res = await handler(gEvent); + if (res) return; + } + }; - public removeNewMessageEventHandler(handler: MessageHandler) { - this.onMessageHandlers.includes(handler) && - this.onMessageHandlers.splice(this.onMessageHandlers.indexOf(handler), 1); - } + private onGroupMemberDecrease = async (event: MemberDecreaseEvent) => { + const gEvent = new GroupMemberDecreaseEvent(event.group, event.user_id, event.operator_id, event.dismiss); + for (const handler of this.onGroupMemberDecreaseHandlers) { + const res = await handler(gEvent); + if (res) return; + } + }; - public getChat(roomId: number): Group | Friend { - if (roomId > 0) { - return this.pickFriend(roomId); + private onFriendIncrease = async (event: OicqFriendIncreaseEvent) => { + const gEvent = new FriendIncreaseEvent(event.friend); + for (const handler of this.onFriendIncreaseHandlers) { + const res = await handler(gEvent); + if (res) return; } - else { - return this.pickGroup(-roomId); + }; + + private onMessageRecall = async (event: FriendRecallEvent | GroupRecallEvent) => { + const gEvent = new MessageRecallEvent('friend' in event ? event.friend : event.group, event.seq, event.rand, event.time); + for (const handler of this.onMessageRecallHandlers) { + const res = await handler(gEvent); + if (res) return; } - } + }; + + private onPoke = async (event: FriendPokeEvent | GroupPokeEvent) => { + const gEvent = new PokeEvent('friend' in event ? event.friend : event.group, event.operator_id, event.target_id, event.action, event.suffix); + for (const handler of this.onPokeHandlers) { + const res = await handler(gEvent); + if (res) return; + } + }; + + private onFriendRequest = async (event: FriendRequestEvent) => { + for (const handler of this.onFriendRequestHandlers) { + const res = await handler(event); + if (res) return; + } + }; + + private onGroupInvite = async (event: GroupInviteEvent) => { + for (const handler of this.onGroupInviteHandlers) { + const res = await handler(event); + if (res) return; + } + }; public async makeForwardMsgSelf(msglist: Forwardable[] | Forwardable, dm?: boolean): Promise<{ resid: string, @@ -173,27 +254,27 @@ export default class OicqClient extends Client { let imgs: Image[] = []; let cnt = 0; for (const fake of msglist) { - const maker = new Converter(fake.message, { dm, cachedir: this.config.data_dir }); + const maker = new Converter(fake.message, { dm, cachedir: this.oicq.config.data_dir }); makers.push(maker); const seq = randomBytes(2).readInt16BE(); const rand = randomBytes(4).readInt32BE(); let nickname = String(fake.nickname || fake.user_id); if (!nickname && fake instanceof PrivateMessage) - nickname = this.fl.get(fake.user_id)?.nickname || this.sl.get(fake.user_id)?.nickname || nickname; + nickname = this.oicq.fl.get(fake.user_id)?.nickname || this.oicq.sl.get(fake.user_id)?.nickname || nickname; if (cnt < 4) { cnt++; } nodes.push({ 1: { 1: fake.user_id, - 2: this.uin, + 2: this.oicq.uin, 3: dm ? 166 : 82, 4: dm ? 11 : null, 5: seq, 6: fake.time || timestamp(), 7: rand2uuid(rand), 9: dm ? null : { - 1: this.uin, + 1: this.oicq.uin, 4: nickname, }, 14: dm ? nickname : null, @@ -209,7 +290,7 @@ export default class OicqClient extends Client { } for (const maker of makers) imgs = [...imgs, ...maker.imgs]; - const contact = (dm ? this.pickFriend : this.pickGroup)(this.uin); + const contact = await (dm ? this.pickFriend : this.pickGroup)(this.oicq.uin); if (imgs.length) await contact.uploadImages(imgs); const compressed = await gzip(pb.encode({ @@ -228,4 +309,58 @@ export default class OicqClient extends Client { resid, }; } + + async pickFriend(uin: number) { + return this.oicq.pickFriend(uin); + } + + async pickGroup(groupId: number) { + return this.oicq.pickGroup(groupId); + } + + async getFriendsWithCluster() { + const result = [] as { name: string, friends: Friend[] }[]; + const friends = Array.from(this.oicq.fl.values()); + for (const [clusterId, name] of this.oicq.classes) { + result.push({ + name, + friends: await Promise.all(friends.filter(f => f.class_id === clusterId).map(f => this.pickFriend(f.user_id))), + }); + } + return result; + } + + async getGroupList() { + return await Promise.all(Array.from(this.oicq.gl.values()).map(g => this.pickGroup(g.group_id))); + } + + override async createSpoilerImageEndpoint(image: ImageElem, nickname: string, title?: string) { + const msgList: Forwardable[] = [{ + user_id: this.oicq.uin, + nickname, + message: image, + }]; + if (title) { + msgList.push({ + user_id: this.oicq.uin, + nickname, + message: title, + }); + } + const fake = await this.makeForwardMsgSelf(msgList); + return [{ + type: 'xml', + id: 60, + data: `` + + `${escapeXml(nickname)}Spoiler 图片${title ? `${escapeXml(title)}` : '' + }请谨慎查看`.replaceAll('\n', ''), + } as XmlElem]; + } } diff --git a/main/src/client/QQClient/entity.ts b/main/src/client/QQClient/entity.ts new file mode 100644 index 00000000..2063bbd6 --- /dev/null +++ b/main/src/client/QQClient/entity.ts @@ -0,0 +1,66 @@ +import type { ForwardMessage, MessageRet, Quotable, Sendable } from '@icqqjs/icqq'; +import { GfsFileStat } from '@icqqjs/icqq/lib/gfs'; +import { Gender, GroupRole } from '@icqqjs/icqq/lib/common'; + +export interface QQEntity { + readonly dm: boolean; + + getForwardMsg(resid: string, fileName?: string): Promise; + + getVideoUrl(fid: string, md5: string | Buffer): Promise; + + recallMsg(paramOrMessageId: number, rand?: number, timeOrPktNum?: number): Promise; + + sendMsg(content: Sendable, source?: Quotable): Promise; + + getForwardMsg(resid: string, fileName?: string): Promise; + + getFileUrl(fid: string): Promise; +} + +export interface QQUser extends QQEntity { + readonly uid: number; +} + +export interface Friend extends QQUser { + readonly nickname: string; + readonly remark: string; + + poke(self?: boolean): Promise; + + sendFile(file: string | Buffer | Uint8Array, filename?: string, callback?: (percentage: string) => void): Promise; +} + +export interface Group extends QQEntity { + readonly gid: number; + readonly name: string; + readonly is_owner: boolean; + readonly is_admin: boolean; + readonly fs: GroupFs; + + pickMember(uid: number, strict?: boolean): GroupMember; + + pokeMember(uid: number): Promise; + + muteMember(uid: number, duration?: number): Promise; + + setCard(uid: number, card?: string): Promise; +} + +export interface GroupFs { + upload(file: string | Buffer | Uint8Array, pid?: string, name?: string, callback?: (percentage: string) => void): Promise; +} + +export interface GroupMember extends QQUser { +} + +export interface GroupMemberInfo { + readonly card: string; + readonly nickname: string; + readonly sex: Gender; + readonly age: number; + readonly join_time: number; + readonly last_sent_time: number; + readonly role: GroupRole; + readonly title: string; +} diff --git a/main/src/client/QQClient/events.ts b/main/src/client/QQClient/events.ts new file mode 100644 index 00000000..a149f173 --- /dev/null +++ b/main/src/client/QQClient/events.ts @@ -0,0 +1,113 @@ +import { Friend, Group } from './index'; +import type { Sendable, MessageElem } from '@icqqjs/icqq'; + +export abstract class ChatEvent { + protected constructor( + public readonly chat: Friend | Group, + ) { + } + + public get dm() { + return 'uid' in this.chat; + } + + public get chatId() { + if ('uid' in this.chat) + return this.chat.uid; + return -this.chat.gid; + } +} + +export class MessageEvent extends ChatEvent { + constructor( + public readonly from: { + id: number; + name: string; + nickname?: string; + card?: string; + }, + chat: Friend | Group, + public readonly message: MessageElem[], + public readonly seq: number, + public readonly rand: number, + public readonly pktnum: number, + public readonly time: number, + public readonly brief: string, + public readonly replyTo: { + fromId: number; + time: number; + seq: number; + rand: number; + message: MessageElem[]; + }, + public readonly anonymous: { + name: string; + } | undefined, + // use only in fetchMsg + public readonly messageId: string, + public readonly atMe: boolean, + public readonly atAll: boolean, + ) { + super(chat); + } + + public reply(content: Sendable, replyTo = true) { + return this.chat.sendMsg(content, replyTo ? { + message: this.message, + seq: this.seq, + rand: this.rand, + time: this.time, + user_id: this.from.id, + } : undefined); + } +} + +export class GroupMemberIncreaseEvent extends ChatEvent { + constructor( + group: Group, + public readonly userId: number, + public readonly nickname: string, + ) { + super(group); + } +} + +export class GroupMemberDecreaseEvent extends ChatEvent { + constructor( + group: Group, + public readonly userId: number, + public readonly operatorId: number, + public readonly dismiss: boolean, + ) { + super(group); + } +} + +export class FriendIncreaseEvent { + constructor( + public readonly friend: Friend, + ) { + } +} + +export class MessageRecallEvent { + constructor( + public readonly chat: Friend | Group, + public readonly seq: number, + public readonly rand: number, + public readonly time: number, + ) { + } +} + +export class PokeEvent extends ChatEvent { + constructor( + chat: Friend | Group, + public readonly fromId: number, + public readonly targetId: number, + public readonly action: string, + public readonly suffix: string, + ) { + super(chat); + } +} diff --git a/main/src/client/QQClient/index.ts b/main/src/client/QQClient/index.ts new file mode 100644 index 00000000..114e3112 --- /dev/null +++ b/main/src/client/QQClient/index.ts @@ -0,0 +1,191 @@ +import OicqClient, { CreateOicqParams } from '../OicqClient'; +import { Friend, Group } from './entity'; +import { + FriendIncreaseEvent, + GroupMemberDecreaseEvent, + GroupMemberIncreaseEvent, + MessageEvent, + MessageRecallEvent, PokeEvent, +} from './events'; +import type { FriendRequestEvent, GroupInviteEvent, ImageElem, MessageElem } from '@icqqjs/icqq'; + +export * from './events'; +export * from './entity'; + +export interface CreateQQClientParamsBase { + id: number; +} + +export type CreateQQClientParams = CreateOicqParams; + +export abstract class QQClient { + protected constructor( + // 数据库内 ID + public readonly id: number, + ) { + } + + public abstract uin: number; + public abstract nickname: string; + + public abstract isOnline(): boolean; + + + private static existedBots = {} as { [id: number]: Promise }; + + public static create(params: CreateQQClientParams) { + if (this.existedBots[params.id]) { + return this.existedBots[params.id]; + } + + let client: Promise; + + switch (params.type) { + case 'oicq': + client = OicqClient.create(params as CreateOicqParams); + break; + default: + throw new Error('Unknown client type'); + } + + this.existedBots[params.id] = client; + return client; + } + + + public abstract getFriendsWithCluster(): Promise<{ + name: string; + friends: Friend[]; + }[]>; + + public abstract getGroupList(): Promise; + + + // Handlers + protected readonly onMessageHandlers: Array<(e: MessageEvent) => Promise> = []; + + public addNewMessageEventHandler(handler: (e: MessageEvent) => Promise) { + this.onMessageHandlers.push(handler); + } + + public removeNewMessageEventHandler(handler: (e: MessageEvent) => Promise) { + this.onMessageHandlers.includes(handler) && + this.onMessageHandlers.splice(this.onMessageHandlers.indexOf(handler), 1); + } + + // + protected readonly onGroupMemberIncreaseHandlers: Array<(e: GroupMemberIncreaseEvent) => Promise> = []; + + public addGroupMemberIncreaseEventHandler(handler: (e: GroupMemberIncreaseEvent) => Promise) { + this.onGroupMemberIncreaseHandlers.push(handler); + } + + public removeGroupMemberIncreaseEventHandler(handler: (e: GroupMemberIncreaseEvent) => Promise) { + this.onGroupMemberIncreaseHandlers.includes(handler) && + this.onGroupMemberIncreaseHandlers.splice(this.onGroupMemberIncreaseHandlers.indexOf(handler), 1); + } + + // + protected readonly onGroupMemberDecreaseHandlers: Array<(e: GroupMemberDecreaseEvent) => Promise> = []; + + public addGroupMemberDecreaseEventHandler(handler: (e: GroupMemberDecreaseEvent) => Promise) { + this.onGroupMemberDecreaseHandlers.push(handler); + } + + public removeGroupMemberDecreaseEventHandler(handler: (e: GroupMemberDecreaseEvent) => Promise) { + this.onGroupMemberDecreaseHandlers.includes(handler) && + this.onGroupMemberDecreaseHandlers.splice(this.onGroupMemberDecreaseHandlers.indexOf(handler), 1); + } + + // + protected readonly onFriendIncreaseHandlers: Array<(e: FriendIncreaseEvent) => Promise> = []; + + public addFriendIncreaseEventHandler(handler: (e: FriendIncreaseEvent) => Promise) { + this.onFriendIncreaseHandlers.push(handler); + } + + public removeFriendIncreaseEventHandler(handler: (e: FriendIncreaseEvent) => Promise) { + this.onFriendIncreaseHandlers.includes(handler) && + this.onFriendIncreaseHandlers.splice(this.onFriendIncreaseHandlers.indexOf(handler), 1); + } + + // + protected readonly onMessageRecallHandlers: Array<(e: MessageRecallEvent) => Promise> = []; + + public addMessageRecallEventHandler(handler: (e: MessageRecallEvent) => Promise) { + this.onMessageRecallHandlers.push(handler); + } + + public removeMessageRecallEventHandler(handler: (e: MessageRecallEvent) => Promise) { + this.onMessageRecallHandlers.includes(handler) && + this.onMessageRecallHandlers.splice(this.onMessageRecallHandlers.indexOf(handler), 1); + } + + // + protected readonly onPokeHandlers: Array<(e: PokeEvent) => Promise> = []; + + public addPokeEventHandler(handler: (e: PokeEvent) => Promise) { + this.onPokeHandlers.push(handler); + } + + public removePokeEventHandler(handler: (e: PokeEvent) => Promise) { + this.onPokeHandlers.includes(handler) && + this.onPokeHandlers.splice(this.onPokeHandlers.indexOf(handler), 1); + } + + // + protected readonly onFriendRequestHandlers: Array<(e: FriendRequestEvent) => Promise> = []; + + public addFriendRequestEventHandler(handler: (e: FriendRequestEvent) => Promise) { + this.onFriendRequestHandlers.push(handler); + } + + public removeFriendRequestEventHandler(handler: (e: FriendRequestEvent) => Promise) { + this.onFriendRequestHandlers.includes(handler) && + this.onFriendRequestHandlers.splice(this.onFriendRequestHandlers.indexOf(handler), 1); + } + + // + protected readonly onGroupInviteHandlers: Array<(e: GroupInviteEvent) => Promise> = []; + + public addGroupInviteEventHandler(handler: (e: GroupInviteEvent) => Promise) { + this.onGroupInviteHandlers.push(handler); + } + + public removeGroupInviteEventHandler(handler: (e: GroupInviteEvent) => Promise) { + this.onGroupInviteHandlers.includes(handler) && + this.onGroupInviteHandlers.splice(this.onGroupInviteHandlers.indexOf(handler), 1); + } + + // End Handlers + + public getChat(roomId: number): Promise { + if (roomId > 0) { + return this.pickFriend(roomId); + } + else { + return this.pickGroup(-roomId); + } + } + + public abstract pickFriend(uin: number): Promise; + + public abstract pickGroup(groupId: number): Promise; + + public async createSpoilerImageEndpoint(image: ImageElem, nickname: string, title?: string): Promise { + const res: MessageElem[] = [ + { + type: 'text', + text: '[Spoiler 图片]', + }, + image, + ]; + if (title) { + res.push({ + type: 'text', + text: title, + }); + } + return res; + } +} diff --git a/main/src/constants/commands.ts b/main/src/constants/commands.ts index a9c4c56c..f1070a96 100644 --- a/main/src/constants/commands.ts +++ b/main/src/constants/commands.ts @@ -81,10 +81,6 @@ const groupInChatCommands = [ new Api.BotCommand({ command: 'enable_qq_forward', description: '恢复从QQ转发至TG' }), new Api.BotCommand({ command: 'disable_tg_forward', description: '停止从TG转发至QQ' }), new Api.BotCommand({ command: 'enable_tg_forward', description: '恢复从TG转发至QQ' }), - new Api.BotCommand({ - command: 'recover', - description: '恢复离线期间的 QQ 消息记录到 TG(不稳定功能,管理员专用)', - }), ]; const personalInChatCommands = [ diff --git a/main/src/controllers/AliveCheckController.ts b/main/src/controllers/AliveCheckController.ts index 4de2a837..c2d5d501 100644 --- a/main/src/controllers/AliveCheckController.ts +++ b/main/src/controllers/AliveCheckController.ts @@ -1,13 +1,14 @@ import Instance from '../models/Instance'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { Api } from 'telegram'; +import { QQClient } from '../client/QQClient'; +import OicqClient from '../client/OicqClient'; export default class AliveCheckController { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, private readonly tgUser: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { tgBot.addNewMessageEventHandler(this.handleMessage); } @@ -35,17 +36,17 @@ export default class AliveCheckController { const tgBot = instance.tgBot; const tgUser = instance.tgUser; - const sign = await oicq.getSign('MessageSvc.PbSendMsg', 233, Buffer.alloc(10)); + const sign = oicq instanceof OicqClient ? await oicq.oicq.getSign('MessageSvc.PbSendMsg', 233, Buffer.alloc(10)) : null; const tgUserName = (tgUser.me.username || tgUser.me.usernames.length) ? '@' + (tgUser.me.username || tgUser.me.usernames[0].username) : tgUser.me.firstName; messageParts.push([ `Instance #${instance.id}`, - `QQ ${instance.qqUin}\t` + + `QQ ${instance.qqUin} (${oicq.constructor.name})\t` + `${boolToStr(oicq.isOnline())}`, - `签名服务器\t${boolToStr(sign.length > 0)}`, + ...(oicq instanceof OicqClient ? [`签名服务器\t${boolToStr(sign.length > 0)}`] : []), `TG @${tgBot.me.username}\t${boolToStr(tgBot.isOnline)}`, diff --git a/main/src/controllers/ConfigController.ts b/main/src/controllers/ConfigController.ts index f47f6f86..743c48c9 100644 --- a/main/src/controllers/ConfigController.ts +++ b/main/src/controllers/ConfigController.ts @@ -1,19 +1,20 @@ import { Api } from 'telegram'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import ConfigService from '../services/ConfigService'; import regExps from '../constants/regExps'; -import { - FriendIncreaseEvent, - GroupMessageEvent, - MemberDecreaseEvent, - MemberIncreaseEvent, - PrivateMessageEvent, -} from '@icqqjs/icqq'; import Instance from '../models/Instance'; import { getLogger, Logger } from 'log4js'; import { editFlags } from '../utils/flagControl'; import flags from '../constants/flags'; +import { + Friend, + FriendIncreaseEvent, + GroupMemberDecreaseEvent, + GroupMemberIncreaseEvent, + QQClient, +} from '../client/QQClient'; +import { MessageEvent } from '../client/QQClient'; +import OicqClient from '../client/OicqClient'; export default class ConfigController { private readonly configService: ConfigService; @@ -23,16 +24,16 @@ export default class ConfigController { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, private readonly tgUser: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.log = getLogger(`ConfigController - ${instance.id}`); this.configService = new ConfigService(this.instance, tgBot, tgUser, oicq); tgBot.addNewMessageEventHandler(this.handleMessage); tgBot.addNewServiceMessageEventHandler(this.handleServiceMessage); tgBot.addChannelParticipantEventHandler(this.handleChannelParticipant); oicq.addNewMessageEventHandler(this.handleQqMessage); - oicq.on('notice.group.decrease', this.handleGroupDecrease); - this.instance.workMode === 'personal' && oicq.on('notice.group.increase', this.handleMemberIncrease); - this.instance.workMode === 'personal' && oicq.on('notice.friend.increase', this.handleFriendIncrease); + oicq.addGroupMemberDecreaseEventHandler(this.handleGroupDecrease); + this.instance.workMode === 'personal' && oicq.addGroupMemberIncreaseEventHandler(this.handleMemberIncrease); + this.instance.workMode === 'personal' && oicq.addFriendIncreaseEventHandler(this.handleFriendIncrease); this.instance.workMode === 'personal' && this.configService.setupFilter(); } @@ -71,7 +72,9 @@ export default class ConfigController { await this.configService.migrateAllChats(); return true; case '/login': - await this.oicq.login(); + if (this.oicq instanceof OicqClient) { + await this.oicq.oicq.login(); + } return true; } } @@ -115,28 +118,29 @@ export default class ConfigController { } }; - private handleQqMessage = async (message: GroupMessageEvent | PrivateMessageEvent) => { - if (message.message_type !== 'private' || this.instance.workMode === 'group') return false; + private handleQqMessage = async (message: MessageEvent) => { + if (!message.dm || this.instance.workMode === 'group') return false; if (this.instance.flags & flags.NO_AUTO_CREATE_PM) return false; - const pair = this.instance.forwardPairs.find(message.friend); + const chat = message.chat as Friend; + const pair = this.instance.forwardPairs.find(chat); if (pair) return false; // 如果正在创建中,应该阻塞 - let promise = this.createPrivateMessageGroupBlockList.get(message.from_id); + let promise = this.createPrivateMessageGroupBlockList.get(chat.uid); if (promise) { await promise; return false; } // 有未创建转发群的新私聊消息时自动创建 - promise = this.configService.createGroupAndLink(message.from_id, message.friend.remark || message.friend.nickname, true); - this.createPrivateMessageGroupBlockList.set(message.from_id, promise); + promise = this.configService.createGroupAndLink(chat.uid, chat.remark || chat.nickname, true); + this.createPrivateMessageGroupBlockList.set(chat.uid, promise); await promise; return false; }; - private handleMemberIncrease = async (event: MemberIncreaseEvent) => { - if (event.user_id !== this.oicq.uin || this.instance.forwardPairs.find(event.group)) return; + private handleMemberIncrease = async (event: GroupMemberIncreaseEvent) => { + if (event.userId !== this.oicq.uin || this.instance.forwardPairs.find(event.chat)) return; // 是新群并且是自己加入了 - await this.configService.promptNewQqChat(event.group); + await this.configService.promptNewQqChat(event.chat); }; private handleFriendIncrease = async (event: FriendIncreaseEvent) => { @@ -157,11 +161,11 @@ export default class ConfigController { } }; - private handleGroupDecrease = async (event: MemberDecreaseEvent) => { + private handleGroupDecrease = async (event: GroupMemberDecreaseEvent) => { // 如果是自己被踢出群,则删除对应的配置 // 如果是群主解散群,则删除对应的配置 - if (event.user_id !== this.oicq.uin) return; - const pair = this.instance.forwardPairs.find(event.group); + if (event.userId !== this.oicq.uin) return; + const pair = this.instance.forwardPairs.find(event.chat); if (!pair) return; await this.instance.forwardPairs.remove(pair); this.log.info(`已删除关联 ID: ${pair.dbId}`); diff --git a/main/src/controllers/DeleteMessageController.ts b/main/src/controllers/DeleteMessageController.ts index 4869f438..4b5f52cf 100644 --- a/main/src/controllers/DeleteMessageController.ts +++ b/main/src/controllers/DeleteMessageController.ts @@ -1,10 +1,9 @@ import DeleteMessageService from '../services/DeleteMessageService'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { Api } from 'telegram'; -import { FriendRecallEvent, GroupRecallEvent } from '@icqqjs/icqq'; import { DeletedMessageEvent } from 'telegram/events/DeletedMessage'; import Instance from '../models/Instance'; +import { MessageRecallEvent, QQClient } from '../client/QQClient'; export default class DeleteMessageController { private readonly deleteMessageService: DeleteMessageService; @@ -12,13 +11,12 @@ export default class DeleteMessageController { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, private readonly tgUser: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.deleteMessageService = new DeleteMessageService(this.instance, tgBot); tgBot.addNewMessageEventHandler(this.onTelegramMessage); tgBot.addEditedMessageEventHandler(this.onTelegramEditMessage); tgUser.addDeletedMessageEventHandler(this.onTgDeletedMessage); - oicq.on('notice.friend.recall', this.onQqRecall); - oicq.on('notice.group.recall', this.onQqRecall); + oicq.addMessageRecallEventHandler(this.onQqRecall); } private onTelegramMessage = async (message: Api.Message) => { @@ -42,8 +40,8 @@ export default class DeleteMessageController { return await this.onTelegramMessage(message); }; - private onQqRecall = async (event: FriendRecallEvent | GroupRecallEvent) => { - const pair = this.instance.forwardPairs.find('friend' in event ? event.friend : event.group); + private onQqRecall = async (event: MessageRecallEvent) => { + const pair = this.instance.forwardPairs.find(event.chat); if (!pair) return; await this.deleteMessageService.handleQqRecall(event, pair); }; diff --git a/main/src/controllers/FileAndFlashPhotoController.ts b/main/src/controllers/FileAndFlashPhotoController.ts index 1e606be5..17d22d68 100644 --- a/main/src/controllers/FileAndFlashPhotoController.ts +++ b/main/src/controllers/FileAndFlashPhotoController.ts @@ -1,12 +1,11 @@ import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { Api } from 'telegram'; import db from '../models/db'; -import { Button } from 'telegram/tl/custom/button'; import { getLogger, Logger } from 'log4js'; import { CustomFile } from 'telegram/client/uploads'; import { fetchFile, getImageUrlByMd5 } from '../utils/urls'; import Instance from '../models/Instance'; +import { QQClient } from '../client/QQClient'; const REGEX = /^\/start (file|flash)-(\d+)$/; @@ -15,7 +14,7 @@ export default class FileAndFlashPhotoController { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { tgBot.addNewMessageEventHandler(this.onTelegramMessage); this.log = getLogger(`FileAndFlashPhotoController - ${instance.id}`); } @@ -40,7 +39,7 @@ export default class FileAndFlashPhotoController { const fileInfo = await db.file.findFirst({ where: { id }, }); - const downloadUrl = await this.oicq.getChat(Number(fileInfo.roomId)).getFileUrl(fileInfo.fileId); + const downloadUrl = await (await this.oicq.getChat(Number(fileInfo.roomId))).getFileUrl(fileInfo.fileId); await message.reply({ message: fileInfo.info + `\n下载`, }); diff --git a/main/src/controllers/ForwardController.ts b/main/src/controllers/ForwardController.ts index dbb777ce..d3901fe9 100644 --- a/main/src/controllers/ForwardController.ts +++ b/main/src/controllers/ForwardController.ts @@ -1,15 +1,5 @@ import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import ForwardService from '../services/ForwardService'; -import { - FaceElem, - Friend, - FriendPokeEvent, - GroupMessageEvent, - GroupPokeEvent, - MemberIncreaseEvent, MessageElem, - PrivateMessageEvent, -} from '@icqqjs/icqq'; import db from '../models/db'; import { Api } from 'telegram'; import { getLogger, Logger } from 'log4js'; @@ -19,6 +9,16 @@ import { CustomFile } from 'telegram/client/uploads'; import forwardHelper from '../helpers/forwardHelper'; import helper from '../helpers/forwardHelper'; import flags from '../constants/flags'; +import { + QQClient, + MessageEvent, + GroupMemberIncreaseEvent, + PokeEvent, + Friend, + Group, + GroupMemberInfo, +} from '../client/QQClient'; +import { Member as OicqGroupMember } from '@icqqjs/icqq'; export default class ForwardController { private readonly forwardService: ForwardService; @@ -28,32 +28,31 @@ export default class ForwardController { private readonly instance: Instance, private readonly tgBot: Telegram, private readonly tgUser: Telegram, - private readonly oicq: OicqClient, + private readonly oicq: QQClient, ) { this.log = getLogger(`ForwardController - ${instance.id}`); this.forwardService = new ForwardService(this.instance, tgBot, oicq); oicq.addNewMessageEventHandler(this.onQqMessage); - oicq.on('notice.group.increase', this.onQqGroupMemberIncrease); - oicq.on('notice.friend.poke', this.onQqPoke); - oicq.on('notice.group.poke', this.onQqPoke); + oicq.addGroupMemberIncreaseEventHandler(this.onQqGroupMemberIncrease); + oicq.addPokeEventHandler(this.onQqPoke); tgBot.addNewMessageEventHandler(this.onTelegramMessage); tgUser.addNewMessageEventHandler(this.onTelegramUserMessage); tgBot.addEditedMessageEventHandler(this.onTelegramMessage); instance.workMode === 'group' && tgBot.addChannelParticipantEventHandler(this.onTelegramParticipant); } - private onQqMessage = async (event: PrivateMessageEvent | GroupMessageEvent) => { + private onQqMessage = async (event: MessageEvent) => { this.log.debug('收到 QQ 消息', event); try { - const target = event.message_type === 'private' ? event.friend : event.group; - const pair = this.instance.forwardPairs.find(target); + const pair = this.instance.forwardPairs.find(event.chat); if (!pair) return; if ((pair.flags | this.instance.flags) & flags.DISABLE_Q2TG) return; // 如果是多张图片的话,是一整条消息,只过一次,所以不受这个判断影响 - let existed = event.message_type === 'private' && await db.message.findFirst({ + // 防止私聊消息重复,icqq bug + let existed = event.dm && await db.message.findFirst({ where: { qqRoomId: pair.qqRoomId, - qqSenderId: event.sender.user_id, + qqSenderId: event.from.id, seq: event.seq, rand: event.rand, pktnum: event.pktnum, @@ -69,9 +68,9 @@ export default class ForwardController { await db.message.create({ data: { qqRoomId: pair.qqRoomId, - qqSenderId: event.sender.user_id, + qqSenderId: event.from.id, time: event.time, - brief: event.raw_message, + brief: event.brief, seq: event.seq, rand: event.rand, pktnum: event.pktnum, @@ -80,14 +79,14 @@ export default class ForwardController { instanceId: this.instance.id, tgMessageText: tgMessage.message, tgFileId: forwardHelper.getMessageDocumentId(tgMessage), - nick: event.nickname, + nick: event.from.name, tgSenderId: BigInt(this.tgBot.me.id.toString()), richHeaderUsed, }, }); await this.forwardService.addToZinc(pair.dbId, tgMessage.id, { - text: event.raw_message, - nick: event.nickname, + text: event.brief, + nick: event.from.name, }); } catch (e) { @@ -143,14 +142,14 @@ export default class ForwardController { } }; - private onQqGroupMemberIncrease = async (event: MemberIncreaseEvent) => { + private onQqGroupMemberIncrease = async (event: GroupMemberIncreaseEvent) => { try { - const pair = this.instance.forwardPairs.find(event.group); + const pair = this.instance.forwardPairs.find(event.chat); if ((pair?.flags | this.instance.flags) & flags.DISABLE_JOIN_NOTICE) return false; - const avatar = await getAvatar(event.user_id); + const avatar = await getAvatar(event.userId); await pair.tg.sendMessage({ file: new CustomFile('avatar.png', avatar.length, '', avatar), - message: `${event.nickname} (${event.user_id}) 加入了本群`, + message: `${event.nickname} (${event.userId}) 加入了本群`, silent: true, }); } @@ -177,40 +176,48 @@ export default class ForwardController { } }; - private onQqPoke = async (event: FriendPokeEvent | GroupPokeEvent) => { - const target = event.notice_type === 'friend' ? event.friend : event.group; - const pair = this.instance.forwardPairs.find(target); + private onQqPoke = async (event: PokeEvent) => { + const pair = this.instance.forwardPairs.find(event.chat); if (!pair) return; if ((pair?.flags | this.instance.flags) & flags.DISABLE_POKE) return; let operatorName: string, targetName: string; - if (target instanceof Friend) { - if (event.operator_id === target.user_id) { - operatorName = target.remark || target.nickname; + if (event.dm) { + const chat = event.chat as Friend; + if (event.fromId === event.chatId) { + operatorName = chat.remark || chat.nickname; } else { operatorName = '你'; } - if (event.operator_id === event.target_id) { + if (event.fromId === event.targetId) { targetName = '自己'; } - else if (event.target_id === target.user_id) { - targetName = target.remark || target.nickname; + else if (event.targetId === event.chatId) { + targetName = chat.remark || chat.nickname; } else { targetName = '你'; } } else { - const operator = target.pickMember(event.operator_id); - await operator.renew(); - operatorName = operator.card || operator.info.nickname; - if (event.operator_id === event.target_id) { + const chat = event.chat as Group; + const operator = chat.pickMember(event.fromId); + let operatorInfo: GroupMemberInfo; + if (operator instanceof OicqGroupMember) { + operatorInfo = await operator.renew(); + } + // TODO: NapCat + operatorName = operatorInfo.card || operatorInfo.nickname; + if (event.fromId === event.targetId) { targetName = '自己'; } else { - const targetUser = target.pickMember(event.target_id); - await targetUser.renew(); - targetName = targetUser.card || targetUser.info.nickname; + const targetUser = chat.pickMember(event.targetId); + let targetInfo: GroupMemberInfo; + if (targetUser instanceof OicqGroupMember) { + targetInfo = await targetUser.renew(); + } + targetName = targetInfo.card || targetInfo.nickname; } } await pair.tg.sendMessage({ diff --git a/main/src/controllers/HugController.ts b/main/src/controllers/HugController.ts index 8105a086..390b42c6 100644 --- a/main/src/controllers/HugController.ts +++ b/main/src/controllers/HugController.ts @@ -1,7 +1,6 @@ import Instance from '../models/Instance'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; -import { AtElem, Group, GroupMessageEvent, PrivateMessageEvent, Sendable } from '@icqqjs/icqq'; +import { AtElem, Sendable } from '@icqqjs/icqq'; import { Pair } from '../models/Pair'; import { Api } from 'telegram'; import db from '../models/db'; @@ -9,6 +8,8 @@ import BigInteger from 'big-integer'; import helper from '../helpers/forwardHelper'; import { getLogger, Logger } from 'log4js'; import flags from '../constants/flags'; +import { MessageEvent, QQClient, Group, GroupMemberInfo } from '../client/QQClient'; +import { Member as OicqMember } from '@icqqjs/icqq/lib/member'; type ActionSubjectTg = { name: string; @@ -31,15 +32,15 @@ export default class { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.log = getLogger(`HugController - ${instance.id}`); oicq.addNewMessageEventHandler(this.onQqMessage); tgBot.addNewMessageEventHandler(this.onTelegramMessage); } - private onQqMessage = async (event: PrivateMessageEvent | GroupMessageEvent) => { - if (event.message_type !== 'group') return; - const pair = this.instance.forwardPairs.find(event.group); + private onQqMessage = async (event: MessageEvent) => { + if (event.dm) return; + const pair = this.instance.forwardPairs.find(event.chat); if (!pair) return; if ((pair.flags | this.instance.flags) & flags.DISABLE_SLASH_COMMAND) return; const chain = [...event.message]; @@ -55,8 +56,8 @@ export default class { if (!action) return; const from: ActionSubject = { from: 'qq', - name: event.nickname, - id: event.sender.user_id, + name: event.from.name, + id: event.from.id, }; let to: ActionSubject; const ats = chain.filter(it => it.type === 'at') as AtElem[]; @@ -68,14 +69,14 @@ export default class { id: ats[0].qq as number, }; } - else if (event.source && event.source.user_id === this.oicq.uin) { + else if (event.replyTo && event.replyTo.fromId === this.oicq.uin) { // 来自 tg const sourceMessage = await db.message.findFirst({ where: { instanceId: this.instance.id, qqRoomId: pair.qqRoomId, - qqSenderId: event.source.user_id, - seq: event.source.seq, + qqSenderId: event.replyTo.fromId, + seq: event.replyTo.seq, // rand: event.source.rand, }, }); @@ -89,19 +90,23 @@ export default class { name: sourceMessage.nick, }; } - else if (event.source) { - const sourceMember = (pair.qq as Group).pickMember(event.source.user_id); + else if (event.replyTo) { + const sourceMember = (pair.qq as Group).pickMember(event.replyTo.fromId); + let memberInfo: GroupMemberInfo; + if (sourceMember instanceof OicqMember) { + memberInfo = sourceMember.info; + } to = { from: 'qq', - name: sourceMember.card || (await sourceMember.getSimpleInfo()).nickname, - id: event.source.user_id, + name: memberInfo.card || memberInfo.nickname, + id: event.replyTo.fromId, }; } else { to = { from: 'qq', name: '自己', - id: event.sender.user_id, + id: event.from.id, }; } await this.sendAction(pair, from, to, action, exec[5]); diff --git a/main/src/controllers/InChatCommandsController.ts b/main/src/controllers/InChatCommandsController.ts index b48f136d..6d0033bd 100644 --- a/main/src/controllers/InChatCommandsController.ts +++ b/main/src/controllers/InChatCommandsController.ts @@ -2,12 +2,10 @@ import InChatCommandsService from '../services/InChatCommandsService'; import { getLogger, Logger } from 'log4js'; import Instance from '../models/Instance'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { Api } from 'telegram'; -import { Group } from '@icqqjs/icqq'; -import RecoverMessageHelper from '../helpers/RecoverMessageHelper'; import flags from '../constants/flags'; import { editFlags } from '../utils/flagControl'; +import { QQClient, Group } from '../client/QQClient'; export default class InChatCommandsController { private readonly service: InChatCommandsService; @@ -17,7 +15,7 @@ export default class InChatCommandsController { private readonly instance: Instance, private readonly tgBot: Telegram, private readonly tgUser: Telegram, - private readonly oicq: OicqClient, + private readonly oicq: QQClient, ) { this.log = getLogger(`InChatCommandsController - ${instance.id}`); this.service = new InChatCommandsService(instance, tgBot, oicq); @@ -48,7 +46,7 @@ export default class InChatCommandsController { messageParts.unshift('0'); case '/mute': if (this.instance.workMode !== 'personal' || !message.senderId?.eq(this.instance.owner)) return false; - if (!(pair.qq instanceof Group)) return true; + if (!('gid' in pair.qq)) return true; await this.service.mute(message, pair, messageParts); return true; case '/forwardoff': @@ -92,11 +90,11 @@ export default class InChatCommandsController { return true; case '/nick': if (this.instance.workMode !== 'personal' || !message.senderId?.eq(this.instance.owner)) return false; - if (!(pair.qq instanceof Group)) return true; + if (!('gid' in pair.qq)) return true; if (!params) { - await message.reply({ - message: `群名片:${pair.qq.pickMember(this.instance.qqUin, true).card}`, - }); + // await message.reply({ + // message: `群名片:${pair.qq.pickMember(this.instance.qqUin, true).card}`, + // }); return true; } const result = await pair.qq.setCard(this.instance.qqUin, params); @@ -104,11 +102,6 @@ export default class InChatCommandsController { message: '设置' + (result ? '成功' : '失败'), }); return true; - case '/recover': - if (!message.senderId.eq(this.instance.owner)) return true; - const helper = new RecoverMessageHelper(this.instance, this.tgBot, this.tgUser, this.oicq, pair, message); - helper.startRecover().then(() => this.log.info('恢复完成')); - return true; case '/search': await message.reply({ message: await this.service.search(messageParts, pair), diff --git a/main/src/controllers/MiraiSkipFilterController.ts b/main/src/controllers/MiraiSkipFilterController.ts index 8ba356bb..1aa81ec5 100644 --- a/main/src/controllers/MiraiSkipFilterController.ts +++ b/main/src/controllers/MiraiSkipFilterController.ts @@ -1,19 +1,19 @@ import Instance from '../models/Instance'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; -import { GroupMessageEvent, MiraiElem, PrivateMessageEvent } from '@icqqjs/icqq'; +import { MiraiElem } from '@icqqjs/icqq'; +import { MessageEvent, QQClient } from '../client/QQClient'; export default class { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, private readonly tgUser: Telegram, - private readonly qqBot: OicqClient) { + private readonly qqBot: QQClient) { qqBot.addNewMessageEventHandler(this.onQqMessage); } // 当 mapInstance 用同服务器其他个人模式账号发送消息后,message mirai 会带 q2tgSkip=true // 防止 bot 重新收到消息再转一圈回来重新转发或者重新响应命令 - private onQqMessage = async (event: PrivateMessageEvent | GroupMessageEvent) => { + private onQqMessage = async (event: MessageEvent) => { if ('friend' in event) return; if (!event.message) return; const messageMirai = event.message.find(it => it.type === 'mirai') as MiraiElem; diff --git a/main/src/controllers/OicqErrorNotifyController.ts b/main/src/controllers/OicqErrorNotifyController.ts index e0f08897..1ca6d373 100644 --- a/main/src/controllers/OicqErrorNotifyController.ts +++ b/main/src/controllers/OicqErrorNotifyController.ts @@ -1,6 +1,7 @@ import Instance from '../models/Instance'; import OicqClient from '../client/OicqClient'; import { throttle } from '../utils/highLevelFunces'; +import { QQClient } from '../client/QQClient'; export default class OicqErrorNotifyController { private sendMessage = throttle((message: string) => { @@ -8,9 +9,12 @@ export default class OicqErrorNotifyController { }, 1000 * 60); public constructor(private readonly instance: Instance, - private readonly oicq: OicqClient) { - oicq.on('system.offline', async ({ message }) => { - await this.sendMessage(`QQ 机器人掉线\n${message}`); - }); + private readonly oicq: QQClient) { + if (oicq instanceof OicqClient) { + oicq.oicq.on('system.offline', async ({ message }) => { + await this.sendMessage(`QQ 机器人掉线\n${message}`); + }); + } + // TODO: NapCat } } diff --git a/main/src/controllers/QuotLyController.ts b/main/src/controllers/QuotLyController.ts index 589d8366..a5f34a95 100644 --- a/main/src/controllers/QuotLyController.ts +++ b/main/src/controllers/QuotLyController.ts @@ -1,35 +1,33 @@ import Instance from '../models/Instance'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { getLogger, Logger } from 'log4js'; -import { Group, GroupMessageEvent, PrivateMessageEvent } from '@icqqjs/icqq'; import { Api } from 'telegram'; import quotly from 'quote-api/methods/generate.js'; import { CustomFile } from 'telegram/client/uploads'; import db from '../models/db'; -import { Message } from '@prisma/client'; import BigInteger from 'big-integer'; import { getAvatarUrl } from '../utils/urls'; import convert from '../helpers/convert'; import { Pair } from '../models/Pair'; import env from '../models/env'; import flags from '../constants/flags'; +import { MessageEvent, QQClient, Group } from '../client/QQClient'; export default class { private readonly log: Logger; constructor(private readonly instance: Instance, private readonly tgBot: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.log = getLogger(`QuotLyController - ${instance.id}`); oicq.addNewMessageEventHandler(this.onQqMessage); tgBot.addNewMessageEventHandler(this.onTelegramMessage); } - private onQqMessage = async (event: PrivateMessageEvent | GroupMessageEvent) => { + private onQqMessage = async (event: MessageEvent) => { if (this.instance.workMode === 'personal') return; - if (event.message_type !== 'group') return; - const pair = this.instance.forwardPairs.find(event.group); + if (event.dm) return; + const pair = this.instance.forwardPairs.find(event.chat); if (!pair) return; const chain = [...event.message]; while (chain.length && chain[0].type !== 'text') { @@ -38,7 +36,7 @@ export default class { const firstElem = chain[0]; if (firstElem?.type !== 'text') return; if (firstElem.text.trim() !== '/q') return; - if (!event.source) { + if (!event.replyTo) { await event.reply('请回复一条消息', true); return true; } @@ -46,8 +44,8 @@ export default class { where: { instanceId: this.instance.id, qqRoomId: pair.qqRoomId, - qqSenderId: event.source.user_id, - seq: event.source.seq, + qqSenderId: event.replyTo.fromId, + seq: event.replyTo.seq, // rand: event.source.rand, }, }); @@ -106,9 +104,9 @@ export default class { }; private async pinMessageOnBothSide(pair: Pair, sourceMessage: Awaited>) { - if (pair.qq instanceof Group) { + if ('gid' in pair.qq) { try { - await pair.qq.addEssence(sourceMessage.seq, Number(sourceMessage.rand)); + // await pair.qq.addEssence(sourceMessage.seq, Number(sourceMessage.rand)); } catch (e) { this.log.warn('无法添加精华消息,群:', pair.qqRoomId, e); @@ -123,7 +121,7 @@ export default class { } } - private async genQuote(message: Message) { + private async genQuote(message: Awaited>) { const GROUP_ANONYMOUS_BOT = 1087968824n; const backgroundColor = '#292232'; @@ -310,7 +308,7 @@ export default class { return Buffer.from(res.image, 'base64'); } - private async sendQuote(pair: Pair, message: Message) { + private async sendQuote(pair: Pair, message: Awaited>) { const image = await this.genQuote(message); const tgMessage = await pair.tg.sendMessage({ diff --git a/main/src/controllers/RequestController.ts b/main/src/controllers/RequestController.ts index 3e4c961b..b5042a0b 100644 --- a/main/src/controllers/RequestController.ts +++ b/main/src/controllers/RequestController.ts @@ -1,21 +1,21 @@ import { getLogger, Logger } from 'log4js'; import Instance from '../models/Instance'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { FriendRequestEvent, GroupInviteEvent } from '@icqqjs/icqq'; import { getAvatar } from '../utils/urls'; import { CustomFile } from 'telegram/client/uploads'; import { Button } from 'telegram/tl/custom/button'; +import { QQClient } from '../client/QQClient'; export default class RequestController { private readonly log: Logger; constructor(private readonly instance: Instance, private readonly tgBot: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.log = getLogger(`RequestController - ${instance.id}`); - oicq.on('request.friend', this.handleRequest); - oicq.on('request.group.invite', this.handleRequest); + oicq.addFriendRequestEventHandler(this.handleRequest); + oicq.addGroupInviteEventHandler(this.handleRequest); } private handleRequest = async (event: FriendRequestEvent | GroupInviteEvent) => { diff --git a/main/src/controllers/SetupController.ts b/main/src/controllers/SetupController.ts index ebf942e4..5931d6f0 100644 --- a/main/src/controllers/SetupController.ts +++ b/main/src/controllers/SetupController.ts @@ -6,19 +6,19 @@ import { Button } from 'telegram/tl/custom/button'; import setupHelper from '../helpers/setupHelper'; import commands from '../constants/commands'; import { WorkMode } from '../types/definitions'; -import OicqClient from '../client/OicqClient'; import { md5Hex } from '../utils/hashing'; import Instance from '../models/Instance'; import env from '../models/env'; +import { QQClient } from '../client/QQClient'; export default class SetupController { private readonly setupService: SetupService; private readonly log: Logger; private isInProgress = false; - private waitForFinishCallbacks: Array<(ret: { tgUser: Telegram, oicq: OicqClient }) => unknown> = []; + private waitForFinishCallbacks: Array<(ret: { tgUser: Telegram, oicq: QQClient }) => unknown> = []; // 创建的 UserBot private tgUser: Telegram; - private oicq: OicqClient; + private oicq: QQClient; constructor(private readonly instance: Instance, private readonly tgBot: Telegram) { @@ -74,7 +74,8 @@ export default class SetupController { // 登录 oicq if (this.instance.qq) { await this.setupService.informOwner('正在登录已设置好的 QQ'); - this.oicq = await OicqClient.create({ + this.oicq = await QQClient.create({ + type: 'oicq', id: this.instance.qq.id, uin: Number(this.instance.qq.uin), password: this.instance.qq.password, @@ -172,7 +173,7 @@ export default class SetupController { } public waitForFinish() { - return new Promise<{ tgUser: Telegram, oicq: OicqClient }>(resolve => { + return new Promise<{ tgUser: Telegram, oicq: QQClient }>(resolve => { this.waitForFinishCallbacks.push(resolve); }); } diff --git a/main/src/controllers/StatusReportController.ts b/main/src/controllers/StatusReportController.ts deleted file mode 100644 index c74a9381..00000000 --- a/main/src/controllers/StatusReportController.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Instance from '../models/Instance'; -import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; - -export default class { - constructor(private readonly instance: Instance, - private readonly tgBot: Telegram, - private readonly tgUser: Telegram, - private readonly qqBot: OicqClient) { - setInterval(() => this.report(), 1000 * 60); - this.report(); - } - - private async report() { - if (!this.instance.reportUrl) return; - let offline = [] as string[]; - if (!this.tgBot?.isOnline) { - offline.push('tgBot'); - } - if (!this.tgUser?.isOnline) { - offline.push('tgUser'); - } - if (!this.qqBot?.isOnline()) { - offline.push('qqBot'); - } - const online = !offline.length; - const url = new URL(this.instance.reportUrl); - url.searchParams.set('status', online ? 'up' : 'down'); - url.searchParams.set('msg', online ? 'OK' : offline.join(',')); - const res = await fetch(url); - } -} diff --git a/main/src/helpers/RecoverMessageHelper.ts b/main/src/helpers/RecoverMessageHelper.ts deleted file mode 100644 index 170a865d..00000000 --- a/main/src/helpers/RecoverMessageHelper.ts +++ /dev/null @@ -1,370 +0,0 @@ -import Instance from '../models/Instance'; -import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; -import { Pair } from '../models/Pair'; -import { Api } from 'telegram'; -import { GroupMessage, PrivateMessage } from '@icqqjs/icqq'; -import db from '../models/db'; -import { format } from 'date-and-time'; -import lottie from '../constants/lottie'; -import helper from './forwardHelper'; -import convert from './convert'; -import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls'; -import { getLogger, Logger } from 'log4js'; -import path from 'path'; -import exts from '../constants/exts'; -import silk from '../encoding/silk'; -import { md5Hex } from '../utils/hashing'; -import axios from 'axios'; -import { CustomFile } from 'telegram/client/uploads'; -import fsP from 'fs/promises'; -import { file } from 'tmp-promise'; -import env from '../models/env'; - -export default class { - private readonly log: Logger; - - constructor(private readonly instance: Instance, - private readonly tgBot: Telegram, - private readonly tgUser: Telegram, - private readonly oicq: OicqClient, - private readonly pair: Pair, - private readonly requestMessage: Api.Message) { - this.log = getLogger(`MessageRecoverSession - ${instance.id} ${pair.qqRoomId}`); - } - - private statusMessage: Api.Message; - private historyMessages = [] as (PrivateMessage | GroupMessage)[]; - private currentStatus = 'getMessage' as - 'getMessage' | 'getMedia' | 'uploadMessage' | 'uploadMedia' | 'finishing' | 'done'; - private importTxt = ''; - // id to path - private filesMap = {} as { [p: string]: string }; - private mediaUploadedCount = 0; - - public async startRecover() { - await this.updateStatusMessage(); - await this.getMessages(); - this.currentStatus = 'getMedia'; - await this.messagesToTxt(); - this.currentStatus = 'uploadMessage'; - await this.updateStatusMessage(); - await this.importMessagesAndMedia(); - this.currentStatus = 'done'; - await this.updateStatusMessage(); - } - - private async getMessages() { - let timeOrSeq = undefined as number; - while (true) { - const messages = await this.pair.qq.getChatHistory(timeOrSeq); - if (!messages.length) return; - let messagesAllExist = true; - timeOrSeq = messages[0] instanceof GroupMessage ? messages[0].seq : messages[0].time; - for (let i = messages.length - 1; i >= 0; i--) { - const where: { - instanceId: number, - qqSenderId: number, - qqRoomId: number, - seq: number, - rand?: number - } = { - instanceId: this.instance.id, - qqSenderId: messages[i].sender.user_id, - qqRoomId: this.pair.qqRoomId, - seq: messages[i].seq, - }; - if (messages[i] instanceof PrivateMessage) { - where.rand = messages[i].rand; - } - const dbMessage = await db.message.findFirst({ where }); - if (!dbMessage) { - messagesAllExist = false; - this.historyMessages.unshift(messages[i]); - } - } - await this.updateStatusMessage(); - if (messagesAllExist) return; - } - } - - private async messagesToTxt() { - let lastMediaCount = 0; - for (const message of this.historyMessages) { - let text = ''; - const useFile = (fileKey: string, filePath: string) => { - if (!path.extname(fileKey)) fileKey += '.file'; - this.filesMap[fileKey] = filePath; - this.importTxt += `${format(new Date(message.time * 1000), 'DD/MM/YYYY, HH:mm')} - ` + - `${message.nickname}: ${fileKey} (file attached)\n`; - }; - for (const elem of message.message) { - let url: string; - switch (elem.type) { - case 'text': { - text += elem.text; - break; - } - case 'at': - case 'face': - case 'sface': { - text += `[${elem.text}]`; - break; - } - case 'bface': { - const fileKey = md5Hex(elem.file) + '.webp'; - useFile(fileKey, await convert.webp(fileKey, () => fetchFile(getBigFaceUrl(elem.file)))); - break; - } - case 'video': - // 先获取 URL,要传给下面 - url = await this.pair.qq.getVideoUrl(elem.fid, elem.md5); - case 'image': - case 'flash': - if ('url' in elem) - url = elem.url; - try { - if (elem.type === 'image' && elem.asface && !(elem.file as string).toLowerCase().endsWith('.gif')) { - useFile(elem.file as string, await convert.webp(elem.file as string, () => fetchFile(elem.url))); - } - else { - useFile(elem.file as string, await convert.cachedBuffer(elem.file as string, () => fetchFile(url))); - } - } - catch (e) { - this.log.error('下载媒体失败', e); - // 下载失败让 Telegram 服务器下载 - text += ` ${url} `; - } - break; - case 'file': { - const extName = path.extname(elem.name); - // 50M 以下文件下载转发 - if (elem.size < 1024 * 1024 * 50 || exts.images.includes(extName.toLowerCase())) { - // 是图片 - let url = await this.pair.qq.getFileUrl(elem.fid); - if (url.includes('?fname=')) { - url = url.split('?fname=')[0]; - // Request path contains unescaped characters - } - this.log.info('正在下载媒体,长度', helper.hSize(elem.size)); - try { - useFile(elem.name, await convert.cachedBuffer(elem.name, () => fetchFile(url))); - } - catch (e) { - this.log.error('下载媒体失败', e); - text += `文件: ${helper.htmlEscape(elem.name)}\n` + - `大小: ${helper.hSize(elem.size)}`; - } - } - else { - text += `文件: ${helper.htmlEscape(elem.name)}\n` + - `大小: ${helper.hSize(elem.size)}`; - } - break; - } - case 'record': { - useFile(elem.md5 + '.ogg', await convert.cached(elem.md5 + '.ogg', - async (output) => await silk.decode(await fetchFile(elem.url), output))); - break; - } - case 'share': { - text += elem.url; - break; - } - case 'json': { - text += helper.processJson(elem.data); - break; - } - case 'xml': { - const result = helper.processXml(elem.data); - switch (result.type) { - case 'text': - text += helper.htmlEscape(result.text); - break; - case 'image': - try { - useFile(result.md5, await convert.cachedBuffer(result.md5, () => fetchFile(getImageUrlByMd5(result.md5)))); - } - catch (e) { - this.log.error('下载媒体失败', e); - text += ` ${getImageUrlByMd5(result.md5)} `; - } - break; - case 'forward': - if (env.CRV_API) { - try { - const messages = await this.pair.qq.getForwardMsg(result.resId); - const hash = md5Hex(result.resId); - text += `转发的消息记录 ${env.CRV_API}/?hash=${hash}`; - // 传到 Cloudflare - axios.post(`${env.CRV_API}/add`, { - auth: env.CRV_KEY, - key: hash, - data: messages, - }) - .then(data => this.log.trace('上传消息记录到 Cloudflare', data.data)) - .catch(e => this.log.error('上传消息记录到 Cloudflare 失败', e)); - } - catch (e) { - text += '[转发多条消息(无法获取)]'; - } - } - else { - text += '[转发多条消息]'; - } - break; - } - break; - } - case 'rps': - case 'dice': - text += `[${elem.type === 'rps' ? '猜拳' : '骰子'}] ${elem.id}`; - break; - case 'poke': - text += `[戳一戳] ${elem.text}`; - break; - case 'location': - text += `[位置] ${elem.name}\n${elem.address}`; - break; - } - } - if (text) { - this.importTxt += `${format(new Date(message.time * 1000), 'DD/MM/YYYY, HH:mm')} - ` + - `${message.nickname}: ${text}\n`; - } - if (lastMediaCount !== Object.keys(this.filesMap).length) { - lastMediaCount = Object.keys(this.filesMap).length; - await this.updateStatusMessage(); - } - } - } - - private async importMessagesAndMedia() { - const tgChatForUser = await this.tgUser.getChat(this.pair.tgId); - const txtBuffer = Buffer.from(this.importTxt, 'utf-8'); - const importSession = await tgChatForUser.startImportSession( - new CustomFile('record.txt', txtBuffer.length, '', txtBuffer), - Object.keys(this.filesMap).length, - ); - this.currentStatus = 'uploadMedia'; - await this.updateStatusMessage(); - const { fileTypeFromFile } = await (Function('return import("file-type")')() as Promise); - for (const [fileKey, filePath] of Object.entries(this.filesMap)) { - let type = fileKey.endsWith('.tgs') ? { - ext: 'tgs', - mime: 'application/x-tgsticker', - } : await fileTypeFromFile(filePath); - if (!type) { - type = { - ext: 'bin', - mime: 'application/octet-stream', - }; - } - let media: Api.TypeInputMedia; - if (['.webp', '.tgs'].includes(path.extname(filePath))) { - // 贴纸 - media = new Api.InputMediaUploadedDocument({ - file: await importSession.uploadFile(new CustomFile( - fileKey, - (await fsP.stat(filePath)).size, - filePath, - )), - mimeType: type.mime, - attributes: [], - }); - } - else if (type.mime.startsWith('audio/')) { - // 语音 - media = new Api.InputMediaUploadedDocument({ - file: await importSession.uploadFile(new CustomFile( - fileKey, - (await fsP.stat(filePath)).size, - filePath, - )), - mimeType: type.mime, - attributes: [ - new Api.DocumentAttributeAudio({ - duration: 0, - voice: true, - }), - ], - }); - } - else if (type.ext === 'gif') { - media = new Api.InputMediaUploadedDocument({ - file: await importSession.uploadFile(new CustomFile( - fileKey, - (await fsP.stat(filePath)).size, - filePath, - )), - mimeType: type.mime, - attributes: [new Api.DocumentAttributeAnimated()], - }); - } - else if (type.mime.startsWith('image/')) { - media = new Api.InputMediaUploadedPhoto({ - file: await importSession.uploadFile(new CustomFile( - fileKey, - (await fsP.stat(filePath)).size, - filePath, - )), - }); - } - else { - media = new Api.InputMediaUploadedDocument({ - file: await importSession.uploadFile(new CustomFile( - fileKey, - (await fsP.stat(filePath)).size, - filePath, - )), - mimeType: type.mime, - attributes: [], - }); - } - await importSession.uploadMedia(fileKey, media); - this.mediaUploadedCount++; - await this.updateStatusMessage(); - } - this.currentStatus = 'finishing'; - await this.updateStatusMessage(); - await importSession.finish(); - } - - private lastUpdateStatusTime = 0; - - private async updateStatusMessage() { - if (new Date().getTime() - this.lastUpdateStatusTime < 2000) return; - this.lastUpdateStatusTime = new Date().getTime(); - const statusMessageText = [] as string[]; - switch (this.currentStatus) { - case 'finishing': - statusMessageText.unshift('正在完成…'); - case 'uploadMedia': - statusMessageText.unshift(`正在上传媒体… ${this.mediaUploadedCount}`); - case 'uploadMessage': - statusMessageText.unshift('正在上传消息…'); - case 'getMedia': - statusMessageText.unshift(`正在下载媒体… ${Object.keys(this.filesMap).length}`); - case 'getMessage': - statusMessageText.unshift(`正在获取消息… ${this.historyMessages.length}`); - break; - case 'done': - statusMessageText.unshift(`成功`); - } - if (!this.statusMessage) { - this.statusMessage = await this.requestMessage.reply({ - message: statusMessageText.join('\n'), - }); - } - else { - try { - await this.statusMessage.edit({ - text: statusMessageText.join('\n'), - }); - } - catch (e) { - } - } - } -} diff --git a/main/src/index.ts b/main/src/index.ts index 6a18a14f..ed075060 100644 --- a/main/src/index.ts +++ b/main/src/index.ts @@ -3,6 +3,7 @@ import Instance from './models/Instance'; import db from './models/db'; import api from './api'; import env from './models/env'; +import './models/posthog'; (async () => { configure({ diff --git a/main/src/models/ForwardPairs.ts b/main/src/models/ForwardPairs.ts index abcb3343..78fda71a 100644 --- a/main/src/models/ForwardPairs.ts +++ b/main/src/models/ForwardPairs.ts @@ -1,6 +1,5 @@ -import { Friend, Group } from '@icqqjs/icqq'; +import { Friend, Group, QQClient } from '../client/QQClient'; import TelegramChat from '../client/TelegramChat'; -import OicqClient from '../client/OicqClient'; import Telegram from '../client/Telegram'; import db from './db'; import { Entity } from 'telegram/define'; @@ -18,13 +17,13 @@ export default class ForwardPairs { } // 在 forwardController 创建时初始化 - private async init(oicq: OicqClient, tgBot: Telegram, tgUser: Telegram) { + private async init(oicq: QQClient, tgBot: Telegram, tgUser: Telegram) { const dbValues = await db.forwardPair.findMany({ where: { instanceId: this.instanceId }, }); for (const i of dbValues) { try { - const qq = oicq.getChat(Number(i.qqRoomId)); + const qq = await oicq.getChat(Number(i.qqRoomId)); const tg = await tgBot.getChat(Number(i.tgChatId)); const tgUserChat = await tgUser.getChat(Number(i.tgChatId)); if (qq && tg && tgUserChat) { @@ -37,7 +36,7 @@ export default class ForwardPairs { } } - public static async load(instanceId: number, oicq: OicqClient, tgBot: Telegram, tgUser: Telegram) { + public static async load(instanceId: number, oicq: QQClient, tgBot: Telegram, tgUser: Telegram) { const instance = new this(instanceId); await instance.init(oicq, tgBot, tgUser); return instance; @@ -46,7 +45,7 @@ export default class ForwardPairs { public async add(qq: Friend | Group, tg: TelegramChat, tgUser: TelegramChat) { const dbEntry = await db.forwardPair.create({ data: { - qqRoomId: qq instanceof Friend ? qq.user_id : -qq.group_id, + qqRoomId: 'uid' in qq ? qq.uid : -qq.gid, tgChatId: Number(tg.id), instanceId: this.instanceId, }, @@ -64,11 +63,11 @@ export default class ForwardPairs { public find(target: Friend | Group | TelegramChat | Entity | number | BigInteger) { if (!target) return null; - if (target instanceof Friend) { - return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target.user_id); + if (typeof target === 'object' && 'uid' in target) { + return this.pairs.find(e => 'uid' in e.qq && e.qq.uid === target.uid); } - else if (target instanceof Group) { - return this.pairs.find(e => e.qq instanceof Group && e.qq.group_id === target.group_id); + else if (typeof target === 'object' && 'gid' in target) { + return this.pairs.find(e => 'gid' in e.qq && e.qq.gid === target.gid); } else if (typeof target === 'number' || 'eq' in target) { return this.pairs.find(e => e.qqRoomId === target || e.tg.id.eq(target)); @@ -84,7 +83,7 @@ export default class ForwardPairs { const instanceTgUserId = instance.userMe.id.toString(); if (forwardPair.instanceMapForTg[instanceTgUserId]) continue; try { - const group = instance.oicq.getChat(forwardPair.qqRoomId) as Group; + const group = await instance.oicq.getChat(forwardPair.qqRoomId) as Group; if (!group) continue; forwardPair.instanceMapForTg[instanceTgUserId] = group; this.log.info('MapInstance', { group: forwardPair.qqRoomId, tg: instanceTgUserId, qq: instance.qqUin }); diff --git a/main/src/models/Instance.ts b/main/src/models/Instance.ts index d7a056f7..c1c9d737 100644 --- a/main/src/models/Instance.ts +++ b/main/src/models/Instance.ts @@ -6,7 +6,6 @@ import ForwardController from '../controllers/ForwardController'; import DeleteMessageController from '../controllers/DeleteMessageController'; import FileAndFlashPhotoController from '../controllers/FileAndFlashPhotoController'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { getLogger, Logger } from 'log4js'; import ForwardPairs from './ForwardPairs'; import InstanceManageController from '../controllers/InstanceManageController'; @@ -18,14 +17,13 @@ import RequestController from '../controllers/RequestController'; import OicqErrorNotifyController from '../controllers/OicqErrorNotifyController'; import { MarkupLike } from 'telegram/define'; import { Button } from 'telegram/tl/custom/button'; -import { CustomFile } from 'telegram/client/uploads'; import { QqBot } from '@prisma/client'; -import StatusReportController from '../controllers/StatusReportController'; import HugController from '../controllers/HugController'; import QuotLyController from '../controllers/QuotLyController'; import MiraiSkipFilterController from '../controllers/MiraiSkipFilterController'; import env from './env'; import AliveCheckController from '../controllers/AliveCheckController'; +import { QQClient } from '../client/QQClient'; export default class Instance { public static readonly instances: Instance[] = []; @@ -43,7 +41,7 @@ export default class Instance { public tgBot: Telegram; public tgUser: Telegram; - public oicq: OicqClient; + public oicq: QQClient; private _ownerChat: TelegramChat; @@ -57,7 +55,6 @@ export default class Instance { private inChatCommandsController: InChatCommandsController; private forwardController: ForwardController; private fileAndFlashPhotoController: FileAndFlashPhotoController; - private statusReportController: StatusReportController; private hugController: HugController; private quotLyController: QuotLyController; private miraiSkipFilterController: MiraiSkipFilterController; @@ -125,7 +122,8 @@ export default class Instance { this.log.info('TG UserBot 登录完成'); this._ownerChat = await this.tgBot.getChat(this.owner); this.log.debug('正在登录 OICQ'); - this.oicq = await OicqClient.create({ + this.oicq = await QQClient.create({ + type: 'oicq', id: this.qq.id, uin: Number(this.qq.uin), password: this.qq.password, @@ -144,7 +142,6 @@ export default class Instance { }); this.log.info('OICQ 登录完成'); } - this.statusReportController = new StatusReportController(this, this.tgBot, this.tgUser, this.oicq); this.forwardPairs = await ForwardPairs.load(this.id, this.oicq, this.tgBot, this.tgUser); this.setupCommands() .then(() => this.log.info('命令设置成功')) diff --git a/main/src/models/Pair.ts b/main/src/models/Pair.ts index 0fdf6d84..ae8ee5bd 100644 --- a/main/src/models/Pair.ts +++ b/main/src/models/Pair.ts @@ -1,5 +1,5 @@ import { getLogger } from 'log4js'; -import { Friend, Group } from '@icqqjs/icqq'; +import { Friend, Group } from '../client/QQClient'; import TelegramChat from '../client/TelegramChat'; import getAboutText from '../utils/getAboutText'; import { md5 } from '../utils/hashing'; @@ -53,7 +53,7 @@ export class Pair { } get qqRoomId() { - return this.qq instanceof Friend ? this.qq.user_id : -this.qq.group_id; + return 'uid' in this.qq ? this.qq.uid : -this.qq.gid; } get tgId() { diff --git a/main/src/services/ConfigService.ts b/main/src/services/ConfigService.ts index a5764454..903ef84c 100644 --- a/main/src/services/ConfigService.ts +++ b/main/src/services/ConfigService.ts @@ -1,17 +1,16 @@ import Telegram from '../client/Telegram'; -import { Friend, FriendInfo, Group, GroupInfo } from '@icqqjs/icqq'; import { Button } from 'telegram/tl/custom/button'; import { getLogger, Logger } from 'log4js'; import { getAvatar } from '../utils/urls'; import { CustomFile } from 'telegram/client/uploads'; import db from '../models/db'; import { Api, utils } from 'telegram'; -import OicqClient from '../client/OicqClient'; import { md5 } from '../utils/hashing'; import TelegramChat from '../client/TelegramChat'; import Instance from '../models/Instance'; import getAboutText from '../utils/getAboutText'; import random from '../utils/random'; +import { Friend, Group, QQClient } from '../client/QQClient'; const DEFAULT_FILTER_ID = 114; // 514 @@ -22,7 +21,7 @@ export default class ConfigService { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, private readonly tgUser: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.log = getLogger(`ConfigService - ${instance.id}`); this.owner = tgBot.getChat(this.instance.owner); } @@ -35,17 +34,17 @@ export default class ConfigService { // 开始添加转发群组流程 public async addGroup() { - const qGroups = Array.from(this.oicq.gl).map(e => e[1]) - .filter(it => !this.instance.forwardPairs.find(-it.group_id)); + const qGroups = (await this.oicq.getGroupList()) + .filter(it => !this.instance.forwardPairs.find(-it.gid)); const buttons = qGroups.map(e => this.instance.workMode === 'personal' ? [Button.inline( - `${e.group_name} (${e.group_id})`, + `${e.name} (${e.gid})`, this.tgBot.registerCallback(() => this.onSelectChatPersonal(e)), )] : [Button.url( - `${e.group_name} (${e.group_id})`, - this.getAssociateLink(-e.group_id), + `${e.name} (${e.gid})`, + this.getAssociateLink(-e.gid), )]); await (await this.owner).createPaginatedInlineSelector( '选择 QQ 群组' + (this.instance.workMode === 'group' ? '\n然后选择在 TG 中的群组' : ''), buttons); @@ -53,39 +52,26 @@ export default class ConfigService { // 只可能是 personal 运行模式 public async addFriend() { - const classes = Array.from(this.oicq.classes); - const friends = Array.from(this.oicq.fl).map(e => e[1]); - classes.sort((a, b) => { - if (a[1] < b[1]) { - return -1; - } - else if (a[1] == b[1]) { - return 0; - } - else { - return 1; - } - }); - await (await this.owner).createPaginatedInlineSelector('选择分组', classes.map(e => [ - Button.inline(e[1], this.tgBot.registerCallback( - () => this.openFriendSelection(friends.filter(f => f.class_id === e[0]), e[1]), + const friends = await this.oicq.getFriendsWithCluster(); + await (await this.owner).createPaginatedInlineSelector('选择分组', friends.map(e => [ + Button.inline(e.name, this.tgBot.registerCallback( + () => this.openFriendSelection(e.friends, e.name), )), ])); } - private async openFriendSelection(clazz: FriendInfo[], name: string) { - clazz = clazz.filter(them => !this.instance.forwardPairs.find(them.user_id)); + private async openFriendSelection(clazz: Friend[], name: string) { + clazz = clazz.filter(them => !this.instance.forwardPairs.find(them.uid)); await (await this.owner).createPaginatedInlineSelector(`选择 QQ 好友\n分组:${name}`, clazz.map(e => [ - Button.inline(`${e.remark || e.nickname} (${e.user_id})`, this.tgBot.registerCallback( + Button.inline(`${e.remark || e.nickname} (${e.uid})`, this.tgBot.registerCallback( () => this.onSelectChatPersonal(e), )), ])); } - private async onSelectChatPersonal(info: FriendInfo | GroupInfo) { - const roomId = 'user_id' in info ? info.user_id : -info.group_id; - const name = 'user_id' in info ? info.remark || info.nickname : info.group_name; - const entity = this.oicq.getChat(roomId); + private async onSelectChatPersonal(entity: Friend | Group) { + const roomId = 'uid' in entity ? entity.uid : -entity.gid; + const name = 'uid' in entity ? entity.remark || entity.nickname : entity.name; const avatar = await getAvatar(roomId); const message = await (await this.owner).sendMessage({ message: await getAboutText(entity, true), @@ -102,20 +88,20 @@ export default class ConfigService { } public async addExact(gin: number) { - const group = this.oicq.gl.get(gin); + const group = await this.oicq.pickGroup(gin); let avatar: Buffer; try { - avatar = await getAvatar(-group.group_id); + avatar = await getAvatar(-group.gid); } catch (e) { avatar = null; - this.log.error(`加载 ${group.group_name} (${gin}) 的头像失败`, e); + this.log.error(`加载 ${group.name} (${gin}) 的头像失败`, e); } - const message = `${group.group_name}\n${group.group_id}\n${group.member_count} 名成员`; + const message = `${group.name}\n${group.gid}`; await (await this.owner).sendMessage({ message, file: avatar ? new CustomFile('avatar.png', avatar.length, '', avatar) : undefined, - buttons: Button.url('关联 Telegram 群组', this.getAssociateLink(-group.group_id)), + buttons: Button.url('关联 Telegram 群组', this.getAssociateLink(-group.gid)), }); } @@ -131,11 +117,11 @@ export default class ConfigService { public async createGroupAndLink(room: number | Friend | Group, title?: string, status: boolean | Api.Message = true, chat?: TelegramChat) { this.log.info(`创建群组并关联:${room}`); if (typeof room === 'number') { - room = this.oicq.getChat(room); + room = await this.oicq.getChat(room); } if (!title) { // TS 这边不太智能 - if (room instanceof Friend) { + if ('uid' in room) { title = room.remark || room.nickname; } else { @@ -236,13 +222,13 @@ export default class ConfigService { public async promptNewQqChat(chat: Group | Friend) { const message = await (await this.owner).sendMessage({ message: '你' + - (chat instanceof Group ? '加入了一个新的群' : '增加了一' + random.pick('位', '个', '只', '头') + '好友') + + ('gid' in chat ? '加入了一个新的群' : '增加了一' + random.pick('位', '个', '只', '头') + '好友') + ':\n' + await getAboutText(chat, true) + '\n' + '要创建关联群吗', buttons: Button.inline('创建', this.tgBot.registerCallback(async () => { await message.delete({ revoke: true }); - this.createGroupAndLink(chat, chat instanceof Group ? chat.name : chat.remark || chat.nickname); + this.createGroupAndLink(chat, 'gid' in chat ? chat.name : chat.remark || chat.nickname); })), }); return message; @@ -251,11 +237,11 @@ export default class ConfigService { public async createLinkGroup(qqRoomId: number, tgChatId: number) { if (this.instance.workMode === 'group') { try { - const qGroup = this.oicq.getChat(qqRoomId) as Group; + const qGroup = await this.oicq.getChat(qqRoomId) as Group; const tgChat = await this.tgBot.getChat(tgChatId); const tgUserChat = await this.tgUser.getChat(tgChatId); await this.instance.forwardPairs.add(qGroup, tgChat, tgUserChat); - await tgChat.sendMessage(`QQ群:${qGroup.name} (${qGroup.group_id})已与 ` + + await tgChat.sendMessage(`QQ群:${qGroup.name} (${qGroup.gid})已与 ` + `Telegram 群 ${(tgChat.entity as Api.Channel).title} (${tgChatId})关联`); if (!(tgChat.entity instanceof Api.Channel)) { // TODO 添加一个转换为超级群组的方法链接 diff --git a/main/src/services/DeleteMessageService.ts b/main/src/services/DeleteMessageService.ts index fa79cb77..4cda64d7 100644 --- a/main/src/services/DeleteMessageService.ts +++ b/main/src/services/DeleteMessageService.ts @@ -2,12 +2,12 @@ import Telegram from '../client/Telegram'; import { getLogger, Logger } from 'log4js'; import { Api } from 'telegram'; import db from '../models/db'; -import { Friend, FriendRecallEvent, Group, GroupRecallEvent } from '@icqqjs/icqq'; import Instance from '../models/Instance'; import { Pair } from '../models/Pair'; import { consumer } from '../utils/highLevelFunces'; import forwardHelper from '../helpers/forwardHelper'; import flags from '../constants/flags'; +import { MessageRecallEvent, Group, Friend } from '../client/QQClient'; export default class DeleteMessageService { private readonly log: Logger; @@ -74,7 +74,7 @@ export default class DeleteMessageService { // 所以撤回两次 // 不知道哪次会成功,所以就都不发失败提示了 this.recallQqMessage(pair.qq, messageInfo.seq, Number(messageInfo.rand), - pair.qq instanceof Friend ? messageInfo.time : messageInfo.pktnum, + pair.qq.dm ? messageInfo.time : messageInfo.pktnum, pair, isOthersMsg, !!mapQq); await db.message.delete({ where: { id: messageInfo.id }, @@ -146,7 +146,7 @@ export default class DeleteMessageService { } } - public async handleQqRecall(event: FriendRecallEvent | GroupRecallEvent, pair: Pair) { + public async handleQqRecall(event: MessageRecallEvent, pair: Pair) { if (this.lock(`qq-${pair.qqRoomId}-${event.seq}`)) return; try { const message = await db.message.findFirst({ diff --git a/main/src/services/ForwardService.ts b/main/src/services/ForwardService.ts index f799cea7..733565fb 100644 --- a/main/src/services/ForwardService.ts +++ b/main/src/services/ForwardService.ts @@ -1,13 +1,8 @@ import Telegram from '../client/Telegram'; import { FaceElem, - Forwardable, - Group, - GroupMessageEvent, - MessageElem, MessageRet, - MiraiElem, - PrivateMessageEvent, - PttElem, + Group as OicqGroup, + MessageElem, PttElem, Quotable, segment, Sendable, @@ -27,7 +22,7 @@ import fsP from 'fs/promises'; import eviltransform from 'eviltransform'; import silk from '../encoding/silk'; import axios from 'axios'; -import { md5B64, md5Hex } from '../utils/hashing'; +import { md5Hex } from '../utils/hashing'; import Instance from '../models/Instance'; import { Pair } from '../models/Pair'; import OicqClient from '../client/OicqClient'; @@ -38,18 +33,16 @@ import convert from '../helpers/convert'; import { QQMessageSent } from '../types/definitions'; import ZincSearch from 'zincsearch-node'; import { speech as AipSpeechClient } from 'baidu-aip-sdk'; -import random from '../utils/random'; -import { escapeXml } from '@icqqjs/icqq/lib/common'; import Docker from 'dockerode'; import ReplyKeyboardHide = Api.ReplyKeyboardHide; import env from '../models/env'; import { CustomFile } from 'telegram/client/uploads'; import flags from '../constants/flags'; import BigInteger from 'big-integer'; -import { Image } from '@icqqjs/icqq/lib/message'; import probe from 'probe-image-size'; import markdownEscape from 'markdown-escape'; import pastebin from '../utils/pastebin'; +import { MessageEvent, QQClient } from '../client/QQClient'; const NOT_CHAINABLE_ELEMENTS = ['flash', 'record', 'video', 'location', 'share', 'json', 'xml', 'poke']; const IMAGE_MIMES = ['image/jpeg', 'image/png', 'image/apng', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', 'image/avif', 'image/heic', 'image/heif']; @@ -63,7 +56,7 @@ export default class ForwardService { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.log = getLogger(`ForwardService - ${instance.id}`); if (env.ZINC_URL) { this.zincSearch = new ZincSearch({ @@ -79,7 +72,7 @@ export default class ForwardService { env.BAIDU_SECRET_KEY, ); } - if (oicq.signDockerId) { + if (oicq instanceof OicqClient && oicq.signDockerId) { const socket = new Docker({ socketPath: '/var/run/docker.sock' }); const container = socket.getContainer(oicq.signDockerId); this.restartSignCallbackHandle = tgBot.registerCallback(async (event) => { @@ -131,7 +124,7 @@ export default class ForwardService { } } - public async forwardFromQq(event: PrivateMessageEvent | GroupMessageEvent, pair: Pair) { + public async forwardFromQq(event: MessageEvent, pair: Pair) { const tempFiles: FileResult[] = []; try { let message = '', @@ -140,20 +133,20 @@ export default class ForwardService { replyTo = 0, forceDocument = false; let messageHeader = '', sender = ''; - if (event.message_type === 'group') { + if (!event.dm) { // 产生头部,这和工作模式没有关系 - sender = event.sender.card || event.sender.nickname; + sender = event.from.name; if (event.anonymous) { sender = `[${sender}]${event.anonymous.name}`; } if ((pair.flags | this.instance.flags) & flags.COLOR_EMOJI_PREFIX) { - messageHeader += emoji.color(event.sender.user_id); + messageHeader += emoji.color(event.from.id); } messageHeader += `${helper.htmlEscape(sender)}: `; } const useSticker = (file: FileLike) => { files.push(file); - if (event.message_type === 'group') { + if (!event.dm) { buttons.push(Button.inline(`${sender}:`)); messageHeader = ''; } @@ -206,7 +199,7 @@ export default class ForwardService { break; } case 'at': { - if (event.source?.user_id === elem.qq || event.source?.user_id === this.oicq.uin) + if (event.replyTo?.fromId === elem.qq || event.replyTo?.fromId === this.oicq.uin) break; if (env.WEB_ENDPOINT && typeof elem.qq === 'number') { message += `[${helper.htmlEscape(elem.text)}]`; @@ -306,8 +299,8 @@ export default class ForwardService { const temp = await createTempFile({ postfix: '.ogg' }); tempFiles.push(temp); url = elem.url; - if (!url) { - const refetchMessage = await this.oicq.getMsg(event.message_id); + if (!url && this.oicq instanceof OicqClient) { + const refetchMessage = await this.oicq.oicq.getMsg(event.messageId); url = (refetchMessage.message.find(it => it.type === 'record') as PttElem).url; } await silk.decode(await fetchFile(url), temp.path); @@ -383,14 +376,14 @@ export default class ForwardService { message = message.trim(); // 处理回复 - if (event.source) { + if (event.replyTo) { try { const quote = await db.message.findFirst({ where: { qqRoomId: pair.qqRoomId, - seq: event.source.seq, + seq: event.replyTo.seq, // rand: event.source.rand, - qqSenderId: event.source.user_id, + qqSenderId: event.replyTo.fromId, instanceId: this.instance.id, }, }); @@ -401,9 +394,9 @@ export default class ForwardService { message += '\n\n*回复消息找不到'; this.log.error('回复消息找不到', { qqRoomId: pair.qqRoomId, - seq: event.source.seq, - rand: event.source.rand, - qqSenderId: event.source.user_id, + seq: event.replyTo.seq, + rand: event.replyTo.rand, + qqSenderId: event.replyTo.fromId, instanceId: this.instance.id, }); } @@ -414,7 +407,7 @@ export default class ForwardService { } } - if (this.instance.workMode === 'personal' && event.message_type === 'group' && event.atme && !replyTo) { + if (this.instance.workMode === 'personal' && !event.dm && event.atMe && !replyTo) { message += `\n@${this.instance.userMe.usernames?.length ? this.instance.userMe.usernames[0].username : this.instance.userMe.username}`; @@ -431,7 +424,7 @@ export default class ForwardService { else if (files.length) { messageToSend.file = files; } - else if (event.message_type === 'group' && (pair.flags | this.instance.flags) & flags.RICH_HEADER && env.WEB_ENDPOINT + else if (!event.dm && (pair.flags | this.instance.flags) & flags.RICH_HEADER && env.WEB_ENDPOINT // 当消息包含链接时不显示 RICH HEADER && !isContainsUrl(message)) { // 没有文件时才能显示链接预览 @@ -440,7 +433,7 @@ export default class ForwardService { // https://github.com/tdlib/td/blob/437c2d0c6e0ad104022d5ad86ddc8aedc41cb7a8/td/generate/scheme/telegram_api.tl#L1841 // https://github.com/gram-js/gramjs/pull/633 messageToSend.file = new Api.InputMediaWebPage({ - url: helper.generateRichHeaderUrl(pair.apiKey, event.sender.user_id, messageHeader), + url: helper.generateRichHeaderUrl(pair.apiKey, event.from.id, messageHeader), forceSmallMedia: true, optional: true, }); @@ -489,7 +482,7 @@ export default class ForwardService { }, 3000); } - if (this.instance.workMode === 'personal' && event.message_type === 'group' && event.atall) { + if (this.instance.workMode === 'personal' && !event.dm && event.atAll) { await tgMessage.pin({ notify: false }); } return { tgMessage, richHeaderUsed }; @@ -567,37 +560,13 @@ export default class ForwardService { IMAGE_MIMES.includes(message.document?.mimeType)) { if ('spoiler' in message.media && message.media.spoiler) { isSpoilerPhoto = true; - const msgList: Forwardable[] = [{ - user_id: this.oicq.uin, - nickname: messageHeader.substring(0, messageHeader.length - 3), - message: { - type: 'image', - file: await message.downloadMedia({}), - asface: !!message.sticker, - }, - }]; - if (message.message) { - msgList.push({ - user_id: this.oicq.uin, - nickname: messageHeader.substring(0, messageHeader.length - 3), - message: message.message, - }); - } - const fake = await this.oicq.makeForwardMsgSelf(msgList); - chain.push({ - type: 'xml', - id: 60, - data: `` + - `${escapeXml(messageHeader.substring(0, messageHeader.length - 2))}Spoiler 图片${message.message ? `${escapeXml(message.message)}` : '' - }请谨慎查看`.replaceAll('\n', ''), - }); + + chain.push(...await this.oicq.createSpoilerImageEndpoint({ + type: 'image', + file: await message.downloadMedia({}), + asface: !!message.sticker, + }, messageHeader.substring(0, messageHeader.length - 3), message.message)); + brief += '[Spoiler 图片]'; markdownCompatible = false; } @@ -703,7 +672,7 @@ export default class ForwardService { if (file.size.leq(50 * 1024 * 1024)) { chain.push('\n'); useText('文件正在上传中…'); - if (pair.qq instanceof Group) { + if ('gid' in pair.qq) { pair.qq.fs.upload(await message.downloadMedia({}), '/', fileNameAttribute ? fileNameAttribute.fileName : 'file') .catch(err => pair.qq.sendMsg(`上传失败:\n${err.message}`)); @@ -832,7 +801,7 @@ export default class ForwardService { tempFiles.forEach(it => it.cleanup()); return [{ ...messageSent, - senderId: pair.instanceMapForTg[senderId].client.uin, + senderId: pair.instanceMapForTg[senderId] instanceof OicqGroup ? pair.instanceMapForTg[senderId].client.uin : 0,//TODO brief, }]; } @@ -862,7 +831,8 @@ export default class ForwardService { let messageToSend: Sendable = chainableElements; if (chainableElements.some(it => typeof it === 'object' && it.type === 'markdown')) { this.log.debug(chainableElements); - messageToSend = await pair.qq.uploadLongMsg(chainableElements); + throw new Error('markdown 早寄了'); + // messageToSend = await pair.qq.uploadLongMsg(chainableElements); } qqMessages.push({ ...await pair.qq.sendMsg(messageToSend, source), diff --git a/main/src/services/InChatCommandsService.ts b/main/src/services/InChatCommandsService.ts index 422136c8..d4e7efb8 100644 --- a/main/src/services/InChatCommandsService.ts +++ b/main/src/services/InChatCommandsService.ts @@ -1,17 +1,17 @@ import { getLogger, Logger } from 'log4js'; import Instance from '../models/Instance'; import Telegram from '../client/Telegram'; -import OicqClient from '../client/OicqClient'; import { Api } from 'telegram'; import getAboutText from '../utils/getAboutText'; import { Pair } from '../models/Pair'; import { CustomFile } from 'telegram/client/uploads'; import { getAvatar } from '../utils/urls'; import db from '../models/db'; -import { Friend, Group } from '@icqqjs/icqq'; import { format } from 'date-and-time'; import ZincSearch from 'zincsearch-node'; import env from '../models/env'; +import { QQClient, Group, GroupMemberInfo } from '../client/QQClient'; +import { Member as OicqMember } from '@icqqjs/icqq'; export default class InChatCommandsService { private readonly log: Logger; @@ -19,7 +19,7 @@ export default class InChatCommandsService { constructor(private readonly instance: Instance, private readonly tgBot: Telegram, - private readonly oicq: OicqClient) { + private readonly oicq: QQClient) { this.log = getLogger(`InChatCommandsService - ${instance.id}`); if (env.ZINC_URL) { this.zincSearch = new ZincSearch({ @@ -41,9 +41,9 @@ export default class InChatCommandsService { }); if (messageInfo) { let textToSend = ''; - if (pair.qq instanceof Friend) { + if ('uid' in pair.qq) { if (Number(messageInfo.qqSenderId) === pair.qqRoomId) { - textToSend += `发送者:${pair.qq.remark || pair.qq.nickname}(${pair.qq.user_id})\n`; + textToSend += `发送者:${pair.qq.remark || pair.qq.nickname}(${pair.qq.uid})\n`; } else { textToSend += `发送者:${this.oicq.nickname}(${this.oicq.uin})\n`; @@ -51,11 +51,15 @@ export default class InChatCommandsService { } else { const sender = pair.qq.pickMember(Number(messageInfo.qqSenderId)); - await sender.renew(); - textToSend += `发送者:${sender.title ? `「${sender.title}」` : ''}` + - `${sender.card || sender.info.nickname}(${sender.user_id})\n`; - if (sender.info.role !== 'member') { - textToSend += `职务:${sender.info.role === 'owner' ? '群主' : '管理员'}\n`; + let memberInfo: GroupMemberInfo; + if (sender instanceof OicqMember) { + memberInfo = await sender.renew(); + } + + textToSend += `发送者:${memberInfo.title ? `「${memberInfo.title}」` : ''}` + + `${memberInfo.card || memberInfo.nickname}(${sender.uid})\n`; + if (memberInfo.role !== 'member') { + textToSend += `职务:${memberInfo.role === 'owner' ? '群主' : '管理员'}\n`; } } textToSend += `发送时间:${format(new Date(messageInfo.time * 1000), 'YYYY-M-D hh:mm:ss')}`; @@ -108,12 +112,12 @@ export default class InChatCommandsService { target = Number(dbEntry.qqSenderId); } } - if (pair.qq instanceof Group && !target) { + if ('gid' in pair.qq && !target) { await message.reply({ message: '请回复一条消息', }); } - else if (pair.qq instanceof Group) { + else if ('gid' in pair.qq) { await pair.qq.pokeMember(target); } else { @@ -151,7 +155,7 @@ export default class InChatCommandsService { public async mute(message: Api.Message, pair: Pair, args: string[]) { try { const group = pair.qq as Group; - if(!(group.is_admin||group.is_owner)){ + if (!(group.is_admin || group.is_owner)) { await message.reply({ message: '无管理员权限', }); diff --git a/main/src/services/SetupService.ts b/main/src/services/SetupService.ts index cca8fe98..d347f342 100644 --- a/main/src/services/SetupService.ts +++ b/main/src/services/SetupService.ts @@ -3,13 +3,12 @@ import { getLogger, Logger } from 'log4js'; import { BigInteger } from 'big-integer'; import { Platform } from '@icqqjs/icqq'; import { MarkupLike } from 'telegram/define'; -import OicqClient from '../client/OicqClient'; import { Button } from 'telegram/tl/custom/button'; -import { CustomFile } from 'telegram/client/uploads'; import { WorkMode } from '../types/definitions'; import TelegramChat from '../client/TelegramChat'; import Instance from '../models/Instance'; import db from '../models/db'; +import { QQClient } from '../client/QQClient'; export default class SetupService { private owner: TelegramChat; @@ -85,7 +84,8 @@ export default class SetupService { public async createOicq(uin: number, password: string, platform: Platform, signApi: string, signVer: string) { const dbQQBot = await db.qqBot.create({ data: { uin, password, platform, signApi, signVer } }); - return await OicqClient.create({ + return await QQClient.create({ + type: 'oicq', id: dbQQBot.id, uin, password, platform, signApi, signVer, onVerifyDevice: async (phone) => { diff --git a/main/src/utils/getAboutText.ts b/main/src/utils/getAboutText.ts index bcb55244..f3c7f2c3 100644 --- a/main/src/utils/getAboutText.ts +++ b/main/src/utils/getAboutText.ts @@ -1,13 +1,14 @@ -import { Friend, Group } from '@icqqjs/icqq'; +import { Group as OicqGroup } from '@icqqjs/icqq'; +import { Friend, Group } from '../client/QQClient'; export default async function getAboutText(entity: Friend | Group, html: boolean) { let text: string; - if (entity instanceof Friend) { + if ('uid' in entity) { text = `备注:${entity.remark}\n` + `昵称:${entity.nickname}\n` + - `账号:${entity.user_id}`; + `账号:${entity.uid}`; } - else { + else if (entity instanceof OicqGroup) { const owner = entity.pickMember(entity.info.owner_id); await owner.renew(); const self = entity.pickMember(entity.client.uin); @@ -20,6 +21,7 @@ export default async function getAboutText(entity: Friend | Group, html: boolean `${owner.card || owner.info.nickname} (${owner.user_id})` : '') + ((entity.is_admin || entity.is_owner) ? '\n可管理' : ''); } + // TODO: NapCat Group if (!html) { text = text.replace(/<\/?\w+>/g, ''); diff --git a/main/src/utils/urls.ts b/main/src/utils/urls.ts index 7fbd7287..3c435d5c 100644 --- a/main/src/utils/urls.ts +++ b/main/src/utils/urls.ts @@ -1,13 +1,13 @@ import axios from 'axios'; -import { Friend, Group } from '@icqqjs/icqq'; +import { Friend, Group } from '../client/QQClient'; export function getAvatarUrl(room: number | bigint | Friend | Group): string { if (!room) return ''; - if (room instanceof Friend) { - room = room.user_id; + if (typeof room === 'object' && 'uid' in room) { + room = room.uid; } - if (room instanceof Group) { - room = -room.group_id; + if (typeof room === 'object' && 'gid' in room) { + room = -room.gid; } return room < 0 ? `https://p.qlogo.cn/gh/${-room}/${-room}/0` : @@ -34,5 +34,5 @@ export function getAvatar(room: number | Friend | Group) { } export function isContainsUrl(msg: string): boolean { - return msg.includes("https://") || msg.includes("http://") + return msg.includes('https://') || msg.includes('http://'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eba3154d..49987ffe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: dependencies: '@fastify/http-proxy': specifier: ^9.5.0 - version: 9.5.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + version: 9.5.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@fastify/static': specifier: ^7.0.4 version: 7.0.4 @@ -85,6 +85,9 @@ importers: nodejs-base64: specifier: ^2.0.0 version: 2.0.0 + posthog-node: + specifier: ^4.0.1 + version: 4.0.1 prisma: specifier: 5.16.2 version: 5.16.2 @@ -96,7 +99,7 @@ importers: version: 2.4.2 quote-api: specifier: https://github.com/Clansty/quote-api/archive/014b21138afbbe0e12c91b00561414b1e851fc0f.tar.gz - version: https://github.com/Clansty/quote-api/archive/014b21138afbbe0e12c91b00561414b1e851fc0f.tar.gz(bufferutil@4.0.8)(utf-8-validate@6.0.4) + version: https://github.com/Clansty/quote-api/archive/014b21138afbbe0e12c91b00561414b1e851fc0f.tar.gz(bufferutil@4.0.8)(utf-8-validate@5.0.10) sharp: specifier: ^0.33.4 version: 0.33.4 @@ -2491,6 +2494,10 @@ packages: resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} engines: {node: ^10 || ^12 || >=14} + posthog-node@4.0.1: + resolution: {integrity: sha512-rtqm2h22QxLGBrW2bLYzbRhliIrqgZ0k+gF0LkQ1SNdeD06YE5eilV0MxZppFSxC8TfH0+B0cWCuebEnreIDgQ==} + engines: {node: '>=15.0.0'} + prisma@5.16.2: resolution: {integrity: sha512-rFV/xoBR2hBGGlu4LPLQd4U8WVA+tSAmYyFWGPRVfj+xg7N4kiZV4lSk38htSpF+/IuHKzlrbh4SFk8Z18cI8A==} engines: {node: '>=16.13'} @@ -2624,6 +2631,9 @@ packages: resolution: {integrity: sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==} engines: {node: '>=4.0.0'} + rusha@0.8.14: + resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -3028,10 +3038,6 @@ packages: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} - utf-8-validate@6.0.4: - resolution: {integrity: sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==} - engines: {node: '>=6.14.2'} - utif2@4.1.0: resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} @@ -3560,12 +3566,12 @@ snapshots: dependencies: fast-json-stringify: 5.16.1 - '@fastify/http-proxy@9.5.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)': + '@fastify/http-proxy@9.5.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@fastify/reply-from': 9.8.0 fast-querystring: 1.1.2 fastify-plugin: 4.5.1 - ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) + ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -5214,7 +5220,7 @@ snapshots: jsbn@1.1.0: {} - jsdom@16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@6.0.4): + jsdom@16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@5.0.10): dependencies: abab: 2.0.6 acorn: 8.12.0 @@ -5241,7 +5247,7 @@ snapshots: whatwg-encoding: 1.0.5 whatwg-mimetype: 2.3.0 whatwg-url: 8.7.0 - ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4) + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) xml-name-validator: 3.0.0 optionalDependencies: canvas: 2.11.2 @@ -5394,10 +5400,10 @@ snapshots: long@5.2.3: {} - lottie-node@2.0.0(canvas@2.11.2)(jsdom@16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@6.0.4))(lottie-web@5.12.2): + lottie-node@2.0.0(canvas@2.11.2)(jsdom@16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@5.0.10))(lottie-web@5.12.2): dependencies: canvas: 2.11.2 - jsdom: 16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@6.0.4) + jsdom: 16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@5.0.10) lottie-web: 5.12.2 lottie-web@5.12.2: {} @@ -5678,6 +5684,13 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + posthog-node@4.0.1: + dependencies: + axios: 1.7.2 + rusha: 0.8.14 + transitivePeerDependencies: + - debug + prisma@5.16.2: dependencies: '@prisma/engines': 5.16.2 @@ -5729,20 +5742,20 @@ snapshots: quick-format-unescaped@4.0.4: {} - quote-api@https://github.com/Clansty/quote-api/archive/014b21138afbbe0e12c91b00561414b1e851fc0f.tar.gz(bufferutil@4.0.8)(utf-8-validate@6.0.4): + quote-api@https://github.com/Clansty/quote-api/archive/014b21138afbbe0e12c91b00561414b1e851fc0f.tar.gz(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: canvas: 2.11.2 dotenv: 7.0.0 emoji-db: 14.0.1 jimp: 0.22.12 - jsdom: 16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@6.0.4) + jsdom: 16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@5.0.10) koa: 2.15.3 koa-bodyparser: 4.4.1 koa-logger: 3.2.1 koa-ratelimit: 4.3.0 koa-response-time: 2.1.0 koa-router: 7.4.0 - lottie-node: 2.0.0(canvas@2.11.2)(jsdom@16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@6.0.4))(lottie-web@5.12.2) + lottie-node: 2.0.0(canvas@2.11.2)(jsdom@16.7.0(bufferutil@4.0.8)(canvas@2.11.2)(utf-8-validate@5.0.10))(lottie-web@5.12.2) lottie-web: 5.12.2 lru-cache: 5.1.1 object-sizeof: 1.6.3 @@ -5858,6 +5871,8 @@ snapshots: runes@0.4.3: {} + rusha@0.8.14: {} + safe-buffer@5.2.1: {} safe-regex2@3.1.0: @@ -6312,11 +6327,6 @@ snapshots: dependencies: node-gyp-build: 4.8.1 - utf-8-validate@6.0.4: - dependencies: - node-gyp-build: 4.8.1 - optional: true - utif2@4.1.0: dependencies: pako: 1.0.11 @@ -6457,15 +6467,15 @@ snapshots: imurmurhash: 0.1.4 slide: 1.1.6 - ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4): + ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.0.8 - utf-8-validate: 6.0.4 + utf-8-validate: 5.0.10 - ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4): + ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): optionalDependencies: bufferutil: 4.0.8 - utf-8-validate: 6.0.4 + utf-8-validate: 5.0.10 xhr@2.6.0: dependencies: