From ceddf864534b474ca2c69ccbb60778f25184d5f2 Mon Sep 17 00:00:00 2001 From: Clansty Date: Thu, 24 Feb 2022 18:27:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=AF=E4=BB=A5=E8=BD=AC=E5=8F=91?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E4=BA=86=EF=BC=8Csession=20=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 + prisma/schema.prisma | 8 +- src/constants/exts.ts | 3 + src/controllers/ForwardController.ts | 49 +++++++ src/helpers/forwardHelper.ts | 122 ++++++++++++++++++ src/index.ts | 4 +- src/providers/forwardPairs.ts | 8 +- src/services/ConfigService.ts | 30 ++--- src/services/ForwardService.ts | 183 +++++++++++++++++++++++++++ src/utils/urls.ts | 13 +- yarn.lock | 63 +++++++++ 11 files changed, 459 insertions(+), 29 deletions(-) create mode 100644 src/constants/exts.ts create mode 100644 src/controllers/ForwardController.ts create mode 100644 src/helpers/forwardHelper.ts create mode 100644 src/services/ForwardService.ts diff --git a/package.json b/package.json index 55627cf..a93d600 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,13 @@ "dependencies": { "@prisma/client": "latest", "axios": "^0.26.0", + "file-type": "^17.1.1", "log4js": "^6.4.1", + "nodejs-base64": "^2.0.0", "oicq": "^2.2.0", "telegram": "^2.5.0" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 62ad5b4..664699a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,7 +14,7 @@ model Message { id Int @id @default(autoincrement()) qqRoomId Int qqSenderId Int - time DateTime + time Int brief String seq Int rand Int @@ -22,7 +22,7 @@ model Message { tgChatId Int tgMsgId Int - @@unique([qqRoomId, qqSenderId, seq, rand, pktnum]) + @@unique([qqRoomId, qqSenderId, seq, rand, pktnum, time]) @@unique([tgChatId, tgMsgId]) } @@ -35,11 +35,11 @@ model ForwardPair { model File { id Int @id @default(autoincrement()) - groupId Int + roomId Int fileId String info String - @@unique([groupId, fileId]) + @@unique([roomId, fileId]) } model FlashPhoto { diff --git a/src/constants/exts.ts b/src/constants/exts.ts new file mode 100644 index 0000000..23210f0 --- /dev/null +++ b/src/constants/exts.ts @@ -0,0 +1,3 @@ +export default { + images: ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp'] +} diff --git a/src/controllers/ForwardController.ts b/src/controllers/ForwardController.ts new file mode 100644 index 0000000..b44dcbc --- /dev/null +++ b/src/controllers/ForwardController.ts @@ -0,0 +1,49 @@ +import Telegram from '../client/Telegram'; +import OicqClient from '../client/OicqClient'; +import ForwardService from '../services/ForwardService'; +import forwardPairs from '../providers/forwardPairs'; +import { DiscussMessageEvent, Friend, Group, GroupMessageEvent, PrivateMessageEvent } from 'oicq'; +import db from '../providers/db'; +import helper from '../helpers/forwardHelper'; + +export default class ForwardController { + private readonly forwardService: ForwardService; + + constructor(private readonly tgBot: Telegram, + private readonly tgUser: Telegram, + private readonly oicq: OicqClient) { + this.forwardService = new ForwardService(tgBot, oicq); + forwardPairs.init(oicq, tgBot) + .then(() => oicq.on('message', this.onQqMsg)); + } + + private onQqMsg = async (event: PrivateMessageEvent | GroupMessageEvent | DiscussMessageEvent) => { + let target: Friend | Group; + if (event.message_type === 'private') { + target = event.friend; + } + else if (event.message_type === 'group') { + target = event.group; + } + else return; + const pair = forwardPairs.find(target); + if (!pair) return; + const tgMsg = await this.forwardService.forwardFromQq(event, pair); + if (tgMsg) { + // 更新数据库 + await db.message.create({ + data: { + qqRoomId: helper.getRoomId(pair.qq), + qqSenderId: event.sender.user_id, + time: event.time, + brief: event.raw_message, + seq: event.seq, + rand: event.rand, + pktnum: event.pktnum, + tgChatId: Number(pair.tg.id), + tgMsgId: tgMsg.id, + }, + }); + } + }; +} diff --git a/src/helpers/forwardHelper.ts b/src/helpers/forwardHelper.ts new file mode 100644 index 0000000..3528db0 --- /dev/null +++ b/src/helpers/forwardHelper.ts @@ -0,0 +1,122 @@ +import { fetchFile } from '../utils/urls'; +import { CustomFile } from 'telegram/client/uploads'; +import { Friend, Group } from 'oicq'; +import { base64decode } from 'nodejs-base64'; +import { getLogger } from 'log4js'; + +const log = getLogger('ForwardHelper'); + +export default { + async downloadToCustomFile(url: string) { + const { fileTypeFromBuffer } = await import('file-type'); + const file = await fetchFile(url); + const type = await fileTypeFromBuffer(file); + return new CustomFile(`image.${type.ext}`, file.length, '', file); + }, + + hSize(size: number) { + const BYTE = 1024; + + if (size < BYTE) + return size + 'B'; + if (size < Math.pow(BYTE, 2)) + return (size / BYTE).toFixed(1) + 'KB'; + if (size < Math.pow(BYTE, 3)) + return (size / Math.pow(BYTE, 2)).toFixed(1) + 'MB'; + if (size < Math.pow(BYTE, 4)) + return (size / Math.pow(BYTE, 3)).toFixed(1) + 'GB'; + return (size / Math.pow(BYTE, 4)).toFixed(1) + 'TB'; + }, + + htmlEscape: (text: string) => + text.replace(/&/g, '&') + .replace(//g, '>'), + + getRoomId(room: Friend | Group) { + if (room instanceof Friend) { + return room.user_id; + } + else { + return room.group_id; + } + }, + + processJson(json: string) { + const jsonObj = JSON.parse(json); + if (jsonObj.app === 'com.tencent.mannounce') { + try { + const title = base64decode(jsonObj.meta.mannounce.title); + const content = base64decode(jsonObj.meta.mannounce.text); + return title + '\n\n' + content; + } + catch (err) { + log.error('解析群公告时出错', err); + return '[群公告]'; + } + } + let appurl: string; + const biliRegex = /(https?:\\?\/\\?\/b23\.tv\\?\/\w*)\??/; + const zhihuRegex = /(https?:\\?\/\\?\/\w*\.?zhihu\.com\\?\/[^?"=]*)\??/; + const biliRegex2 = /(https?:\\?\/\\?\/\w*\.?bilibili\.com\\?\/[^?"=]*)\??/; + const jsonLinkRegex = /{.*"app":"com.tencent.structmsg".*"jumpUrl":"(https?:\\?\/\\?\/[^",]*)".*}/; + const jsonAppLinkRegex = /"contentJumpUrl": ?"(https?:\\?\/\\?\/[^",]*)"/; + if (biliRegex.test(json)) + appurl = json.match(biliRegex)[1].replace(/\\\//g, '/'); + else if (biliRegex2.test(json)) + appurl = json.match(biliRegex2)[1].replace(/\\\//g, '/'); + else if (zhihuRegex.test(json)) + appurl = json.match(zhihuRegex)[1].replace(/\\\//g, '/'); + else if (jsonLinkRegex.test(json)) + appurl = json.match(jsonLinkRegex)[1].replace(/\\\//g, '/'); + else if (jsonAppLinkRegex.test(json)) + appurl = json.match(jsonAppLinkRegex)[1].replace(/\\\//g, '/'); + if (appurl) { + return appurl; + } + else { + // TODO 记录无法解析的 JSON + return '[JSON]'; + } + }, + + processXml(xml: string): + { type: 'forward', resId: string } | { type: 'text', text: string } | { type: 'image', md5: string } { + const urlRegex = /url="([^"]+)"/; + const md5ImageRegex = /image md5="([A-F\d]{32})"/; + let text: string; + if (urlRegex.test(xml)) + text = xml.match(urlRegex)[1].replace(/\\\//g, '/'); + if (xml.includes('action="viewMultiMsg"')) { + text = '[Forward multiple messages]'; + const resIdRegex = /m_resid="([\w+=/]+)"/; + if (resIdRegex.test(xml)) { + const resId = xml.match(resIdRegex)![1]; + return { + type: 'forward', + resId, + }; + } + } + else if (text) { + text = text.replace(/&/g, '&'); + return { + type: 'text', + text, + }; + } + else if (md5ImageRegex.test(xml)) { + const imgMd5 = xml.match(md5ImageRegex)![1]; + return { + type: 'image', + md5: imgMd5, + }; + } + else { + return { + type: 'text', + text: '[XML]', + }; + } + }, +}; diff --git a/src/index.ts b/src/index.ts index 28db168..a7fd540 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ import Telegram from './client/Telegram'; import { config } from './providers/userConfig'; -import { getLogger, configure } from 'log4js'; +import { configure, getLogger } from 'log4js'; import SetupController from './controllers/SetupController'; import OicqClient from './client/OicqClient'; import ConfigController from './controllers/ConfigController'; +import ForwardController from './controllers/ForwardController'; (async () => { configure({ @@ -49,4 +50,5 @@ import ConfigController from './controllers/ConfigController'; log.debug('OICQ 登录完成'); } new ConfigController(tgBot, tgUser, oicq); + new ForwardController(tgBot, tgUser, oicq); })(); diff --git a/src/providers/forwardPairs.ts b/src/providers/forwardPairs.ts index 1dde79f..36bf08d 100644 --- a/src/providers/forwardPairs.ts +++ b/src/providers/forwardPairs.ts @@ -5,7 +5,7 @@ import OicqClient from '../client/OicqClient'; import Telegram from '../client/Telegram'; import db from './db'; -type Pair = { +export type Pair = { qq: Friend | Group; tg: TelegramChat; } @@ -34,13 +34,17 @@ class ForwardPairsInternal { }); } - public find(target: Friend | Group | TelegramChat | Api.Chat) { + public find(target: Friend | Group | TelegramChat | Api.Chat | number) { if (target instanceof Friend) { return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target.user_id); } 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 === 'number') { + return this.pairs.find(e => e.qq instanceof Friend && e.qq.user_id === target || + e.qq instanceof Group && e.qq.group_id === -target); + } else { return this.pairs.find(e => e.tg.id.eq(target.id)); } diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts index cebf515..66dd16c 100644 --- a/src/services/ConfigService.ts +++ b/src/services/ConfigService.ts @@ -16,14 +16,14 @@ import forwardPairs from '../providers/forwardPairs'; const DEFAULT_FILTER_ID = 114; // 514 export default class ConfigService { - private owner: TelegramChat; + private owner: Promise; private log = getLogger('ConfigService'); private filter: Api.DialogFilter; constructor(private readonly tgBot: Telegram, private readonly tgUser: Telegram, private readonly oicq: OicqClient) { - tgBot.getChat(config.owner).then(e => this.owner = e); + this.owner = tgBot.getChat(config.owner); } private getAssociateLink(roomId: number) { @@ -31,12 +31,11 @@ export default class ConfigService { } public async configCommands() { - // 这个在一初始化好就要调用,所以不能直接用 this.owner await this.tgBot.setCommands([], new Api.BotCommandScopeUsers()); await this.tgBot.setCommands( config.workMode === 'personal' ? commands.personalPrivateCommands : commands.groupPrivateCommands, new Api.BotCommandScopePeer({ - peer: (await this.tgBot.getChat(config.owner)).inputPeer, + peer: (await this.owner).inputPeer, }), ); } @@ -56,7 +55,7 @@ export default class ConfigService { `${e.group_name} (${e.group_id})`, this.getAssociateLink(-e.group_id), )]); - await this.owner.createPaginatedInlineSelector( + await (await this.owner).createPaginatedInlineSelector( '选择 QQ 群组' + (config.workMode === 'group' ? '\n然后选择在 TG 中的群组' : ''), buttons); } @@ -75,7 +74,7 @@ export default class ConfigService { return 1; } }); - await this.owner.createPaginatedInlineSelector('选择分组', classes.map(e => [ + 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]), )), @@ -83,7 +82,7 @@ export default class ConfigService { } private async openFriendSelection(clazz: FriendInfo[], name: string) { - await this.owner.createPaginatedInlineSelector(`选择 QQ 好友\n分组:${name}`, clazz.map(e => [ + await (await this.owner).createPaginatedInlineSelector(`选择 QQ 好友\n分组:${name}`, clazz.map(e => [ Button.inline(`${e.remark || e.nickname} (${e.user_id})`, this.tgBot.registerCallback( () => this.createGroupAndLink(e.user_id, e.remark || e.nickname), )), @@ -101,7 +100,7 @@ export default class ConfigService { this.log.error(`加载 ${group.group_name} (${gin}) 的头像失败`, e); } const message = `${group.group_name}\n${group.group_id}\n${group.member_count} 名成员`; - await this.owner.sendMessage({ + 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)), @@ -125,18 +124,18 @@ export default class ConfigService { let isFinish = false; try { // 状态信息 - const status = await this.owner.sendMessage('正在创建 Telegram 群…'); + const status = await (await this.owner).sendMessage('正在创建 Telegram 群…'); // 创建群聊,拿到的是 user 的 chat const chat = await this.tgUser.createChat({ title, - users: [this.tgBot.me.id], + users: [this.tgBot.me.username], }); - const chatForBot = await this.tgBot.getChat(chat.id); // 设置管理员 await status.edit({ text: '正在设置管理员…' }); await chat.editAdmin(this.tgBot.me.username, true); + const chatForBot = await this.tgBot.getChat(chat.id); // 关联写入数据库 await status.edit({ text: '正在写数据库…' }); @@ -174,7 +173,7 @@ export default class ConfigService { } catch (e) { this.log.error('创建群组并关联失败', e); - await this.owner.sendMessage(`创建群组并关联${isFinish ? '成功了但没完全成功' : '失败'}\n${e}`); + await (await this.owner).sendMessage(`创建群组并关联${isFinish ? '成功了但没完全成功' : '失败'}\n${e}`); } } @@ -190,14 +189,13 @@ export default class ConfigService { catch (e) { message = `错误:${e}`; } - await this.owner.sendMessage({ message }); + await (await this.owner).sendMessage({ message }); } // 创建 QQ 群组的文件夹 public async setupFilter() { const result = await this.tgUser.getDialogFilters(); this.filter = result.find(e => e.id === DEFAULT_FILTER_ID); - this.log.debug(this.filter); if (!this.filter) { this.log.info('创建 TG 文件夹'); // 要自己计算新的 id,随意 id 也是可以的 @@ -222,13 +220,13 @@ export default class ConfigService { if (!isSuccess) { this.filter = null; this.log.error(errorText); - await this.owner.sendMessage(errorText); + await (await this.owner).sendMessage(errorText); } } catch (e) { this.filter = null; this.log.error(errorText, e); - await this.owner.sendMessage(errorText + `\n${e}`); + await (await this.owner).sendMessage(errorText + `\n${e}`); } } } diff --git a/src/services/ForwardService.ts b/src/services/ForwardService.ts new file mode 100644 index 0000000..09c0c47 --- /dev/null +++ b/src/services/ForwardService.ts @@ -0,0 +1,183 @@ +import Telegram from '../client/Telegram'; +import OicqClient from '../client/OicqClient'; +import { GroupMessageEvent, PrivateMessageEvent } from 'oicq'; +import { Pair } from '../providers/forwardPairs'; +import { fetchFile, getBigFaceUrl, getImageUrlByMd5 } from '../utils/urls'; +import { FileLike, MarkupLike } from 'telegram/define'; +import { CustomFile } from 'telegram/client/uploads'; +import { getLogger } from 'log4js'; +import path from 'path'; +import exts from '../constants/exts'; +import helper from '../helpers/forwardHelper'; +import db from '../providers/db'; +import { Button } from 'telegram/tl/custom/button'; +import { SendMessageParams } from 'telegram/client/messages'; + +// noinspection FallThroughInSwitchStatementJS +export default class ForwardService { + private log = getLogger('ForwardService'); + + constructor(private readonly tgBot: Telegram, + private readonly oicq: OicqClient) { + } + + public async forwardFromQq(event: PrivateMessageEvent | GroupMessageEvent, pair: Pair) { + try { + let message = '', files: FileLike[] = [], button: MarkupLike, replyTo = 0; + let messageHeader = ''; + if (event.message_type === 'group') { + // 产生头部,这和工作模式没有关系 + const sender = event.sender.card || event.sender.nickname; + messageHeader = `${helper.htmlEscape(sender)}: `; + } + for (const elem of event.message) { + let url: string; + switch (elem.type) { + case 'text': { + message += elem.text; + break; + } + case 'at': { + if (event.source?.user_id === elem.qq) + break; + } + case 'face': + case 'sface': { + message += `[${elem.text}]`; + break; + } + case 'bface': { + const file = await fetchFile(getBigFaceUrl(elem.file)); + files.push(new CustomFile('face.png', file.length, '', file)); + break; + } + case 'video': + // 先获取 URL,要传给下面 + url = await pair.qq.getVideoUrl(elem.fid, elem.md5); + case 'image': + case 'flash': + // TODO 闪照单独处理 + if ('url' in elem) + url = elem.url; + try { + files.push(await helper.downloadToCustomFile(url)); + } + catch (e) { + this.log.error('下载媒体失败', e); + // 下载失败让 Telegram 服务器下载 + files.push(url); + } + break; + case 'file': { + const extName = path.extname(elem.name); + if (exts.images.includes(extName.toLowerCase())) { + // 是图片 + const url = await pair.qq.getFileUrl(elem.fid); + try { + files.push(await helper.downloadToCustomFile(url)); + } + catch (e) { + this.log.error('下载媒体失败', e); + // 下载失败让 Telegram 服务器下载 + files.push(url); + } + } + else { + message = `文件: ${elem.name}\n` + + `大小: ${helper.hSize(elem.size)}`; + const dbEntry = await db.file.create({ + data: { fileId: elem.fid, roomId: helper.getRoomId(pair.qq), info: message }, + }); + button = Button.url('⏬ 获取下载地址', + `https://t.me/${this.tgBot.me.username}?start=file-${dbEntry.id}`); + } + } + case 'record': { + // TODO + message = '[语音]'; + break; + } + case 'share': { + message = elem.url; + break; + } + case 'json': { + message = helper.processJson(elem.data); + break; + } + case 'xml': { + const result = helper.processXml(elem.data); + switch (result.type) { + case 'text': + message = result.text; + break; + case 'image': + try { + files.push(await helper.downloadToCustomFile(getImageUrlByMd5(result.md5))); + } + catch (e) { + this.log.error('下载媒体失败', e); + // 下载失败让 Telegram 服务器下载 + files.push(getImageUrlByMd5(result.md5)); + } + break; + case 'forward': + // TODO 详细展开 + message = '[转发多条消息]'; + break; + } + break; + } + case 'rps': + case 'dice': + message = `[${elem.type === 'rps' ? '猜拳' : '骰子'}] ${elem.id}`; + break; + case 'poke': + message = `[戳一戳] ${elem.text}`; + break; + case 'location': + message = `[位置] ${elem.name}\n${elem.address}`; + break; + } + } + message = helper.htmlEscape(message.trim()); + message = messageHeader + (message && messageHeader ? '\n' : '') + message; + + // 处理回复 + if (event.source) { + try { + const quote = await db.message.findFirst({ + where: { + qqRoomId: helper.getRoomId(pair.qq), + seq: event.source.seq, + rand: event.source.rand, + }, + }); + if (quote) { + replyTo = quote.tgMsgId; + } + } + catch (e) { + this.log.error('查找回复消息失败', e); + } + } + + // 发送消息 + const messageToSend: SendMessageParams = {}; + message && (messageToSend.message = message); + if (files.length === 1) { + messageToSend.file = files[0]; + } + else if (files.length) { + messageToSend.file = files; + } + button && (messageToSend.buttons = button); + replyTo && (messageToSend.replyTo = replyTo); + + return await pair.tg.sendMessage(messageToSend); + } + catch (e) { + this.log.error('从 QQ 到 TG 到消息转发失败', e); + } + } +} diff --git a/src/utils/urls.ts b/src/utils/urls.ts index f260b6d..8d3ca7f 100644 --- a/src/utils/urls.ts +++ b/src/utils/urls.ts @@ -12,15 +12,16 @@ export function getImageUrlByMd5(md5: string) { } export function getBigFaceUrl(file: string) { - return `https://gxh.vip.qq.com/club/item/parcel/item/${file.substring( - 0, - 2, - )}/${file.substring(0, 32)}/300x300.png`; + return `https://gxh.vip.qq.com/club/item/parcel/item/${file.substring(0, 2)}/${file.substring(0, 32)}/300x300.png`; } -export async function getAvatar(roomId: number): Promise { - const res = await axios.get(getAvatarUrl(roomId), { +export async function fetchFile(url: string): Promise { + const res = await axios.get(url, { responseType: 'arraybuffer', }); return res.data; } + +export function getAvatar(roomId: number) { + return fetchFile(getAvatarUrl(roomId)); +} diff --git a/yarn.lock b/yarn.lock index 945e530..1d456f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -83,6 +83,13 @@ __metadata: languageName: node linkType: hard +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 1d575d02d2a9f0c5a4ca5180635ebd2ad59e0f18b42a65f3d04844148b49b3db35cf00b6012a1af2d59c2ab3caca59451c5689f747ba8667ee586ad717ee58e1 + languageName: node + linkType: hard + "@tootallnate/once@npm:1": version: 1.1.2 resolution: "@tootallnate/once@npm:1.1.2" @@ -613,6 +620,17 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^17.1.1": + version: 17.1.1 + resolution: "file-type@npm:17.1.1" + dependencies: + readable-web-to-node-stream: ^3.0.2 + strtok3: ^7.0.0-alpha.7 + token-types: ^5.0.0-alpha.2 + checksum: fb52169bbb8f4d179ee372f8a8792880c8702f96cf2f9e55094baa6c7e81bed0be347a9f3dd849632597fe0e4c9bd6cc8d60b68292730dc52919844bbb3f2dab + languageName: node + linkType: hard + "flatted@npm:^3.2.4": version: 3.2.5 resolution: "flatted@npm:3.2.5" @@ -1343,6 +1361,13 @@ __metadata: languageName: node linkType: hard +"nodejs-base64@npm:^2.0.0": + version: 2.0.0 + resolution: "nodejs-base64@npm:2.0.0" + checksum: 1e9fc06396bd77caba14f93eec40751f00c4570346ebbaf9ded746f8309d0adc8a18bf62c55a9e0288854df7eb61ee9942b316548fd1e185de59213ce4f0fbde + languageName: node + linkType: hard + "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -1453,6 +1478,13 @@ __metadata: languageName: node linkType: hard +"peek-readable@npm:^5.0.0-alpha.5": + version: 5.0.0-alpha.5 + resolution: "peek-readable@npm:5.0.0-alpha.5" + checksum: cab949ed457dac95ae191dd412c6a0ba05e8db4842fd51704ccf2c8c16d6f3ceeefc997e8caea584a0395f229e468c0203a38a8d0ec68cfef8bacc157a006dcb + languageName: node + linkType: hard + "pngjs@npm:^6.0.0": version: 6.0.0 resolution: "pngjs@npm:6.0.0" @@ -1514,7 +1546,9 @@ __metadata: "@prisma/client": latest "@types/node": ^17.0.18 axios: ^0.26.0 + file-type: ^17.1.1 log4js: ^6.4.1 + nodejs-base64: ^2.0.0 oicq: ^2.2.0 prisma: latest telegram: ^2.5.0 @@ -1544,6 +1578,15 @@ __metadata: languageName: node linkType: hard +"readable-web-to-node-stream@npm:^3.0.2": + version: 3.0.2 + resolution: "readable-web-to-node-stream@npm:3.0.2" + dependencies: + readable-stream: ^3.6.0 + checksum: 8c56cc62c68513425ddfa721954875b382768f83fa20e6b31e365ee00cbe7a3d6296f66f7f1107b16cd3416d33aa9f1680475376400d62a081a88f81f0ea7f9c + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -1746,6 +1789,16 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^7.0.0-alpha.7": + version: 7.0.0-alpha.8 + resolution: "strtok3@npm:7.0.0-alpha.8" + dependencies: + "@tokenizer/token": ^0.3.0 + peek-readable: ^5.0.0-alpha.5 + checksum: 00e5c9ed0c5de537839cf443d5628f0ae88d2956ca1fdcbd45cd97372045d7179a40ec99f6d06b02c59ec2141e362142ad0a87c59506d401dbd3bd1ee242abaa + languageName: node + linkType: hard + "tar@npm:^6.0.2, tar@npm:^6.1.2": version: 6.1.11 resolution: "tar@npm:6.1.11" @@ -1792,6 +1845,16 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^5.0.0-alpha.2": + version: 5.0.0-alpha.2 + resolution: "token-types@npm:5.0.0-alpha.2" + dependencies: + "@tokenizer/token": ^0.3.0 + ieee754: ^1.2.1 + checksum: ee23eeed6f383b1072d99781d62fc7840f1296a96d47e636e36fca757debd7eb4274d31fcd2d56997606eede00b12b1e61a64610fe0ed7807d6b1c4dcf5ccc6b + languageName: node + linkType: hard + "ts-custom-error@npm:^3.2.0": version: 3.2.0 resolution: "ts-custom-error@npm:3.2.0"