perf: 完善导入引擎

This commit is contained in:
Clansty 2022-03-08 21:13:46 +08:00
parent 7477979bd1
commit 5b0e6279a8
No known key found for this signature in database
GPG Key ID: 05F8479BA63A8E92
14 changed files with 477 additions and 127 deletions

BIN
assets/tgs/tgs0.tgs Normal file

Binary file not shown.

BIN
assets/tgs/tgs1.tgs Normal file

Binary file not shown.

BIN
assets/tgs/tgs2.tgs Normal file

Binary file not shown.

BIN
assets/tgs/tgs3.tgs Normal file

Binary file not shown.

BIN
assets/tgs/tgs4.tgs Normal file

Binary file not shown.

BIN
assets/tgs/tgs5.tgs Normal file

Binary file not shown.

View File

@ -6,12 +6,14 @@
"build": "tsc",
"start": "prisma db push --accept-data-loss && node build/index.js",
"prisma": "prisma",
"convert": "ts-node tools/convert"
"import": "ts-node tools/import"
},
"devDependencies": {
"@types/cli-progress": "^3",
"@types/date-and-time": "^0.13.0",
"@types/fluent-ffmpeg": "^2",
"@types/node": "^17.0.18",
"@types/prompts": "^2",
"prisma": "latest",
"ts-node": "^10.5.0",
"tsc": "^2.0.4",
@ -20,13 +22,16 @@
"dependencies": {
"@prisma/client": "latest",
"axios": "^0.26.0",
"cli-progress": "^3.10.0",
"date-and-time": "^2.2.1",
"dotenv": "^16.0.0",
"eviltransform": "^0.2.2",
"file-type": "^17.1.1",
"fluent-ffmpeg": "^2.1.2",
"log4js": "^6.4.1",
"nodejs-base64": "^2.0.0",
"oicq": "^2.2.0",
"prompts": "^2.4.2",
"silk-sdk": "^0.2.2",
"telegram": "^2.5.0",
"tmp-promise": "^3.0.3"

View File

@ -11,12 +11,14 @@ import os from 'os';
import TelegramChat from './TelegramChat';
import TelegramSession from '../models/TelegramSession';
import { LogLevel } from 'telegram/extensions/Logger';
import { CustomFile } from 'telegram/client/uploads';
import { TelegramImportSession } from './TelegramImportSession';
type MessageHandler = (message: Api.Message) => Promise<boolean | void>;
type ServiceMessageHandler = (message: Api.MessageService) => Promise<boolean | void>;
export default class Telegram {
private readonly client: TelegramClient;
public readonly client: TelegramClient;
private waitForMessageHelper: WaitForMessageHelper;
private callbackQueryHelper: CallbackQueryHelper = new CallbackQueryHelper();
private readonly onMessageHandlers: Array<MessageHandler> = [];
@ -24,7 +26,7 @@ export default class Telegram {
private readonly onServiceMessageHandlers: Array<ServiceMessageHandler> = [];
public me: Api.User;
private constructor(sessionId: string) {
private constructor(sessionId: string, appName: string) {
this.client = new TelegramClient(
new TelegramSession(sessionId),
parseInt(process.env.TG_API_ID),
@ -32,7 +34,7 @@ export default class Telegram {
{
connectionRetries: 5,
langCode: 'zh',
deviceModel: `Q2TG On ${os.hostname()}`,
deviceModel: `${appName} On ${os.hostname()}`,
appVersion: 'raincandy',
proxy: process.env.PROXY_IP ? {
socksType: 5,
@ -44,15 +46,15 @@ export default class Telegram {
this.client.logger.setLevel(LogLevel.WARN);
}
public static async create(startArgs: UserAuthParams | BotAuthParams, sessionId: string) {
const bot = new this(sessionId);
public static async create(startArgs: UserAuthParams | BotAuthParams, sessionId: string, appName = 'Q2TG') {
const bot = new this(sessionId, appName);
await bot.client.start(startArgs);
await bot.config();
return bot;
}
public static async connect(sessionId: string) {
const bot = new this(sessionId);
public static async connect(sessionId: string, appName = 'Q2TG') {
const bot = new this(sessionId, appName);
await bot.client.connect();
await bot.config();
return bot;
@ -160,12 +162,27 @@ export default class Telegram {
return await this.client.invoke(new Api.messages.UpdateDialogFilter(params));
}
public async createChat(title: string, about?: string) {
public async createChat(title: string, about = '') {
const updates = await this.client.invoke(new Api.channels.CreateChannel({
title, about,
megagroup: true,
forImport: true,
})) as Api.Updates;
const newChat = updates.chats[0];
return new TelegramChat(this, this.client, newChat, this.waitForMessageHelper);
}
public async startImportSession(chat: TelegramChat, textFile: CustomFile, mediaCount: number) {
const init = await this.client.invoke(
new Api.messages.InitHistoryImport({
peer: chat.entity,
file: await this.client.uploadFile({
file: textFile,
workers: 1,
}),
mediaCount,
}),
);
return new TelegramImportSession(chat, this.client, init.id);
}
}

View File

@ -0,0 +1,35 @@
import TelegramChat from './TelegramChat';
import { BigInteger } from 'big-integer';
import { Api, TelegramClient } from 'telegram';
import { CustomFile } from 'telegram/client/uploads';
export class TelegramImportSession {
constructor(public readonly chat: TelegramChat,
private readonly client: TelegramClient,
private readonly importId: BigInteger) {
}
public async uploadMedia(fileName: string, media: Api.TypeInputMedia) {
return await this.client.invoke(
new Api.messages.UploadImportedMedia({
peer: this.chat.entity,
importId: this.importId,
fileName,
media,
}),
);
}
public async finish() {
return await this.client.invoke(
new Api.messages.StartHistoryImport({
peer: this.chat.id,
importId: this.importId,
}),
);
}
public async uploadFile(file: CustomFile) {
return await this.client.uploadFile({ file, workers: 2 });
}
}

View File

@ -1,117 +0,0 @@
import fsP from 'fs/promises';
import fs from 'fs';
import path from 'path';
import { Message } from './types';
import OicqClient from '../../src/client/OicqClient';
import { Platform } from 'oicq';
import { fetchFile } from '../../src/utils/urls';
import { md5Hex } from '../../src/utils/hashing';
import { format } from 'date-and-time';
import axios from 'axios';
(async () => {
const selfId = Number(process.argv[2]);
const selfName = process.argv[3];
const filePath = process.argv[4];
const outputPath = process.argv[5];
// 可选参数
const account = Number(process.argv[6]);
const password = process.argv[7];
const crvApi = process.argv[8];
const crvKey = process.argv[9];
const oicq = account && await OicqClient.create({
uin: account,
password,
platform: Platform.Android,
onVerifyDevice: () => process.exit(1),
onVerifySlider: () => process.exit(1),
onQrCode: () => process.exit(1),
});
const content = JSON.parse(await fsP.readFile(filePath, 'utf-8')) as Message[];
if (!fs.existsSync(outputPath)) {
await fsP.mkdir(outputPath);
}
const txt = fs.createWriteStream(path.join(outputPath, 'WhatsApp Chat with Cat.txt'), 'utf-8');
content.sort((a, b) => a.time - b.time);
console.log('count:', content.length);
console.log('files:', content.filter(it => it.file).length);
for (const message of content) {
let sender = message.senderId === selfId ? selfName : message.username;
if (message.system) sender = '系统';
const date = new Date(message.time);
if (message.file) {
if (message.file.type.startsWith('image/')) {
try {
let file: Buffer;
if (message.file.url.startsWith('data:image')) {
const base64Data = message.file.url.replace(/^data:image\/\w+;base64,/, '');
file = Buffer.from(base64Data, 'base64');
}
else {
file = await fetchFile(message.file.url);
}
const md5 = md5Hex(file);
await fsP.writeFile(path.join(outputPath, `IMG-${md5}.jpg`), file);
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: IMG-${md5}.jpg (file attached)\n`);
process.stdout.write('.');
}
catch (e) {
process.stdout.write(e.message);
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${message.file.url}\n`);
}
}
else if (message.file.type.startsWith('audio/') && message.file.url.startsWith('data:audio')) {
try {
let file: Buffer;
const base64Data = message.file.url.replace(/^data:audio\/\w+;base64,/, '');
file = Buffer.from(base64Data, 'base64');
const md5 = md5Hex(file);
await fsP.writeFile(path.join(outputPath, `AUDIO-${md5}.mp3`), file);
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: AUDIO-${md5}.mp3 (file attached)\n`);
process.stdout.write('.');
}
catch (e) {
process.stdout.write(e.message);
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${message.file.url}\n`);
}
}
else {
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: 文件: ${message.file.name}\n` +
`${message.file.type}\n`);
}
}
if (message.content) {
const FORWARD_REGEX = /\[Forward: ([A-Za-z0-9\/+=]+)]/;
if (FORWARD_REGEX.test(message.content) && oicq) {
try {
const resId = FORWARD_REGEX.exec(message.content)[1];
const record = await oicq.getForwardMsg(resId);
const hash = md5Hex(resId);
await axios.post(`${crvApi}/add`, {
auth: crvKey,
key: hash,
data: record,
});
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: 转发的消息记录 ${crvApi}/?hash=${hash}\n`);
process.stdout.write('_');
}
catch (e) {
process.stdout.write(e.message);
}
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: 转发的消息记录\n`);
}
else {
txt.write(`${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${message.content}\n`);
}
}
}
txt.end();
await oicq.logout(false);
console.log('转换成功');
})();

235
tools/import/importer.ts Normal file
View File

@ -0,0 +1,235 @@
import Telegram from '../../src/client/Telegram';
import OicqClient from '../../src/client/OicqClient';
import fsP from 'fs/promises';
import { Message } from './types';
import prompts from 'prompts';
import { dir } from 'tmp-promise';
import { Presets, SingleBar } from 'cli-progress';
import { fetchFile } from '../../src/utils/urls';
import { md5Hex } from '../../src/utils/hashing';
import path from 'path';
import { format } from 'date-and-time';
import axios from 'axios';
import { CustomFile } from 'telegram/client/uploads';
import { Api } from 'telegram';
import fs from 'fs';
const TGS_MAP = ['打call', '流泪', '变形', '比心', '庆祝', '鞭炮'].map(text => `[${text}]请使用最新版手机QQ体验新功能`);
export default {
async doImport(filePath: string, telegram: Telegram, oicq: OicqClient, crvApi: string, crvKey: string) {
const { fileTypeFromFile } = await (Function('return import("file-type")')() as Promise<typeof import('file-type')>);
let selfId = Number(process.env.SELF_ID), selfName = process.env.SELF_NAME;
!(selfId && selfName) && ({ selfId, selfName } = await prompts([
{ type: 'number', name: 'selfId', message: '请输入自己的 ID映射消息' },
{ type: 'text', name: 'selfName', message: '请输入自己的 Telegram 名称(映射消息)' },
]));
const { chatName } = await prompts({
type: 'text', name: 'chatName', message: '请输入用于导入的群组名称(即将创建)',
});
console.log('正在读取记录…');
const content = JSON.parse(await fsP.readFile(filePath, 'utf-8')) as Message[];
content.sort((a, b) => a.time - b.time);
let output = '';
const tmpDir = await dir();
const outputPath = tmpDir.path;
const files = new Set<string>();
console.log('正在下载媒体…');
const fileCount = content.filter(it => it.file).length;
const fetchFilesBar = new SingleBar({
hideCursor: true,
format: '{bar} {percentage}% | {value}/{total}',
barsize: 120,
}, Presets.shades_grey);
fetchFilesBar.start(fileCount, 0);
for (const message of content) {
let sender = message.senderId === selfId ? selfName : message.username;
if (message.system) sender = '系统';
const date = new Date(message.time);
if (!message.files?.length && message.file) {
// 适配旧版数据库
message.files = [message.file];
}
if (message.files?.length) {
for (const messageFile of message.files) {
if (messageFile.type.startsWith('image/')) {
try {
let file: Buffer;
if (messageFile.url.startsWith('data:image')) {
const base64Data = messageFile.url.replace(/^data:image\/\w+;base64,/, '');
file = Buffer.from(base64Data, 'base64');
}
else {
file = await fetchFile(messageFile.url);
}
const md5 = md5Hex(file);
await fsP.writeFile(path.join(outputPath, `${md5}.file`), file);
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${md5}.file (file attached)\n`;
files.add(md5);
}
catch (e) {
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${messageFile.url}\n`;
}
}
else if (messageFile.type.startsWith('audio/') && messageFile.url.startsWith('data:audio')) {
try {
let file: Buffer;
const base64Data = messageFile.url.replace(/^data:audio\/\w+;base64,/, '');
file = Buffer.from(base64Data, 'base64');
const md5 = md5Hex(file);
await fsP.writeFile(path.join(outputPath, `${md5}.file`), file);
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${md5}.file (file attached)\n`;
files.add(md5);
}
catch (e) {
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${messageFile.url}\n`;
}
}
else {
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: 文件: ${messageFile.name}\n` +
`${messageFile.type}\n`;
}
}
fetchFilesBar.increment();
}
if (message.content) {
const FORWARD_REGEX = /\[Forward: ([A-Za-z0-9\/+=]+)]/;
const tgsIndex = TGS_MAP.indexOf(message.content);
if (tgsIndex > -1) {
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: tgs${tgsIndex}.file (file attached)\n`;
files.add(`tgs${tgsIndex}`);
}
else if (FORWARD_REGEX.test(message.content) && oicq) {
try {
const resId = FORWARD_REGEX.exec(message.content)[1];
const record = await oicq.getForwardMsg(resId);
const hash = md5Hex(resId);
await axios.post(`${crvApi}/add`, {
auth: crvKey,
key: hash,
data: record,
});
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: 转发的消息记录 ${crvApi}/?hash=${hash}\n`;
}
catch (e) {
}
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: 转发的消息记录\n`;
}
else {
output += `${format(date, 'DD/MM/YYYY, HH:mm')} - ${sender}: ${message.content}\n`;
}
}
}
fetchFilesBar.stop();
// 转换好了,开始导入 TG
console.log('正在准备导入…');
const newChat = await telegram.createChat(chatName);
const txtBuffer = Buffer.from(output, 'utf-8');
const importSession = await telegram.startImportSession(
newChat,
new CustomFile('record.txt', txtBuffer.length, '', txtBuffer),
files.size,
);
console.log('正在上传媒体…');
const uploadMediaBar = new SingleBar({
hideCursor: true,
format: '{bar} {percentage}% | {value}/{total}',
barsize: 120,
}, Presets.shades_grey);
uploadMediaBar.start(files.size, 0);
for (const md5 of files) {
const fileName = md5 + '.file';
const file = md5.startsWith('tgs') ? path.join('./assets/tgs', md5 + '.tgs') : path.join(outputPath, md5 + '.file');
const type = md5.startsWith('tgs') ? {
ext: 'tgs',
mime: 'application/x-tgsticker',
} : await fileTypeFromFile(file);
let media: Api.TypeInputMedia;
if (md5.startsWith('tgs') || type.ext === 'webp') {
// 贴纸
media = new Api.InputMediaUploadedDocument({
file: await importSession.uploadFile(new CustomFile(
`${fileName}.${type.ext}`,
fs.statSync(file).size,
file,
)),
mimeType: type.mime,
attributes: [],
});
}
else if (type.mime.startsWith('audio/')) {
// 语音
media = new Api.InputMediaUploadedDocument({
file: await importSession.uploadFile(new CustomFile(
`${fileName}.${type.ext}`,
fs.statSync(file).size,
file,
)),
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(
`${fileName}.${type.ext}`,
fs.statSync(file).size,
file,
)),
mimeType: type.mime,
attributes: [new Api.DocumentAttributeAnimated()],
});
}
else if (type.mime.startsWith('image/')) {
media = new Api.InputMediaUploadedPhoto({
file: await importSession.uploadFile(new CustomFile(
`${fileName}.${type.ext}`,
fs.statSync(file).size,
file,
)),
});
}
else {
media = new Api.InputMediaUploadedDocument({
file: await importSession.uploadFile(new CustomFile(
`${fileName}.${type.ext}`,
fs.statSync(file).size,
file,
)),
mimeType: type.mime,
attributes: [],
});
}
await importSession.uploadMedia(fileName, media);
uploadMediaBar.increment();
}
await importSession.finish();
await tmpDir.cleanup();
console.log('导入成功!');
},
};

114
tools/import/index.ts Normal file
View File

@ -0,0 +1,114 @@
import prompts from 'prompts';
import db from '../../src/models/db';
import Telegram from '../../src/client/Telegram';
import OicqClient from '../../src/client/OicqClient';
import 'dotenv/config';
import importer from './importer';
(async () => {
if (!(process.env.TG_API_ID && process.env.TG_API_HASH)) {
const { apiId, apiHash } = await prompts([
{ type: 'text', name: 'apiId', message: 'Telegram API ID?' },
{ type: 'text', name: 'apiHash', message: 'Telegram API Hash?' },
]);
process.env.TG_API_ID = apiId;
process.env.TG_API_HASH = apiHash;
}
let sessionName = process.env.SESSION;
!sessionName && ({ sessionName } = await prompts({
type: 'text', name: 'sessionName', message: '输入数据库中的 Session 名称',
}));
let telegram: Telegram;
if (await db.session.findFirst({ where: { name: sessionName } })) {
// Session 存在
telegram = await Telegram.connect(sessionName, 'Chat Importer');
}
else {
const { phoneNumber } = await prompts({
type: 'text', name: 'phoneNumber', message: '请输入手机号码',
});
telegram = await Telegram.create({
phoneNumber,
password: async (hint?: string) => {
const { password } = await prompts({
type: 'password', name: 'password',
message: `请输入你的二步验证密码${hint ? ' 密码提示:' + hint : ''}`,
});
return password;
},
phoneCode: async (isCodeViaApp?: boolean) => {
const { code } = await prompts({
type: 'text', name: 'code',
message: `请输入你${isCodeViaApp ? ' Telegram APP 中' : '手机上'}收到的验证码`,
});
return code;
},
onError: (err) => console.error(err),
}, sessionName, 'Chat Importer');
}
let isLoginOicq = !!(process.env.Q_UIN && process.env.Q_PASSWORD && process.env.Q_PLATFORM);
!isLoginOicq && ({ isLoginOicq } = await prompts({
type: 'toggle', name: 'isLoginOicq', message: '要登录 OICQ 嘛,这样可以获取转发的消息记录',
}));
let oicq: OicqClient,
crvApi = process.env.CRV_API, crvKey = process.env.CRV_KEY;
if (isLoginOicq) {
let uin = Number(process.env.Q_UIN);
let password = process.env.Q_PASSWORD;
let platform = Number(process.env.Q_PLATFORM);
!(uin && password && platform) && ({ uin, password, platform } = await prompts([
{ type: 'number', name: 'uin', message: '请输入账号,可以是任意账号,和导入内容无关' },
{ type: 'password', name: 'password', message: '请输入密码' },
{
type: 'select', name: 'platform', message: '选择登录协议',
choices: [
{ title: '安卓手机', value: '1' },
{ title: '安卓手表', value: '3' },
{ title: 'macOS', value: '4' },
{ title: 'iPad', value: '5' },
],
},
]));
oicq = await OicqClient.create({
uin,
password,
platform,
onVerifyDevice: async (phone) => {
const { code } = await prompts({
type: 'text', name: 'code',
message: `请输入你的手机 ${phone} 收到的验证码`,
});
return code;
},
onVerifySlider: () => {
console.log('出滑块了,暂不支持');
process.exit(1);
},
onQrCode: () => process.exit(1),
});
!(crvApi && crvKey) && ({ crvApi, crvKey } = await prompts([
{ type: 'text', name: 'crvApi', message: 'Chat record viewer API 地址' },
{ type: 'text', name: 'crvKey', message: 'Chat record viewer API Key' },
]));
}
if (process.argv[2]) {
await importer.doImport(process.argv[2], telegram, oicq, crvApi, crvKey);
}
else {
while (true) {
const { filePath } = await prompts({
type: 'text', name: 'filePath', message: '请选择一个导出的 JSON 文件',
});
await importer.doImport(filePath.trim(), telegram, oicq, crvApi, crvKey);
const { isContinue } = await prompts({
type: 'toggle', name: 'isContinue', message: '要继续导入嘛',
});
if (!isContinue) break;
}
}
await oicq.logout(false);
})();

View File

@ -125,6 +125,15 @@ __metadata:
languageName: node
linkType: hard
"@types/cli-progress@npm:^3":
version: 3.9.2
resolution: "@types/cli-progress@npm:3.9.2"
dependencies:
"@types/node": "*"
checksum: 371ecb29c45dce7127b4a8626521d908ce7dfaea6a537fcd491b12aea3a8ff58452eb1b33e0d2e0724c81b56df3930195e521e12fe979518deb5329a7a884a02
languageName: node
linkType: hard
"@types/date-and-time@npm:^0.13.0":
version: 0.13.0
resolution: "@types/date-and-time@npm:0.13.0"
@ -155,6 +164,13 @@ __metadata:
languageName: node
linkType: hard
"@types/prompts@npm:^2":
version: 2.4.0
resolution: "@types/prompts@npm:2.4.0"
checksum: ec75655f07fbc3cb2df60a3e3bdd13bfda0554a16f5541285c4b1cc0610756838a2863dccfc911cbb1385492054c07210512164e3dbef272ec638d631783b278
languageName: node
linkType: hard
"abbrev@npm:1":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
@ -404,6 +420,15 @@ __metadata:
languageName: node
linkType: hard
"cli-progress@npm:^3.10.0":
version: 3.10.0
resolution: "cli-progress@npm:3.10.0"
dependencies:
string-width: ^4.2.0
checksum: 8e22c6265f95598002986c6508d05004bd9f5ee17c06f239d8d59e14f5a7e5605055f5d705a3a7d69f6072ea0752b1c094f28c48a704f9fd00378d7d16f0b46d
languageName: node
linkType: hard
"color-support@npm:^1.1.2":
version: 1.1.3
resolution: "color-support@npm:1.1.3"
@ -556,6 +581,13 @@ __metadata:
languageName: node
linkType: hard
"dotenv@npm:^16.0.0":
version: 16.0.0
resolution: "dotenv@npm:16.0.0"
checksum: 664cebb51f0a9a1d1b930f51f0271e72e26d62feaecc9dc03df39453dd494b4e724809ca480fb3ec3213382b1ed3f791aaeb83569a137f9329ce58efd4853dbf
languageName: node
linkType: hard
"emoji-regex@npm:^8.0.0":
version: 8.0.0
resolution: "emoji-regex@npm:8.0.0"
@ -1179,6 +1211,13 @@ __metadata:
languageName: node
linkType: hard
"kleur@npm:^3.0.3":
version: 3.0.3
resolution: "kleur@npm:3.0.3"
checksum: df82cd1e172f957bae9c536286265a5cdbd5eeca487cb0a3b2a7b41ef959fc61f8e7c0e9aeea9c114ccf2c166b6a8dd45a46fd619c1c569d210ecd2765ad5169
languageName: node
linkType: hard
"lodash.merge@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
@ -1616,16 +1655,30 @@ __metadata:
languageName: node
linkType: hard
"prompts@npm:^2.4.2":
version: 2.4.2
resolution: "prompts@npm:2.4.2"
dependencies:
kleur: ^3.0.3
sisteransi: ^1.0.5
checksum: d8fd1fe63820be2412c13bfc5d0a01909acc1f0367e32396962e737cb2fc52d004f3302475d5ce7d18a1e8a79985f93ff04ee03007d091029c3f9104bffc007d
languageName: node
linkType: hard
"q2tg@workspace:.":
version: 0.0.0-use.local
resolution: "q2tg@workspace:."
dependencies:
"@prisma/client": latest
"@types/cli-progress": ^3
"@types/date-and-time": ^0.13.0
"@types/fluent-ffmpeg": ^2
"@types/node": ^17.0.18
"@types/prompts": ^2
axios: ^0.26.0
cli-progress: ^3.10.0
date-and-time: ^2.2.1
dotenv: ^16.0.0
eviltransform: ^0.2.2
file-type: ^17.1.1
fluent-ffmpeg: ^2.1.2
@ -1633,6 +1686,7 @@ __metadata:
nodejs-base64: ^2.0.0
oicq: ^2.2.0
prisma: latest
prompts: ^2.4.2
silk-sdk: ^0.2.2
telegram: ^2.5.0
tmp-promise: ^3.0.3
@ -1766,6 +1820,13 @@ __metadata:
languageName: node
linkType: hard
"sisteransi@npm:^1.0.5":
version: 1.0.5
resolution: "sisteransi@npm:1.0.5"
checksum: aba6438f46d2bfcef94cf112c835ab395172c75f67453fe05c340c770d3c402363018ae1ab4172a1026a90c47eaccf3af7b6ff6fa749a680c2929bd7fa2b37a4
languageName: node
linkType: hard
"slide@npm:^1.1.5":
version: 1.1.6
resolution: "slide@npm:1.1.6"
@ -1837,7 +1898,7 @@ __metadata:
languageName: node
linkType: hard
"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.2.3":
"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
version: 4.2.3
resolution: "string-width@npm:4.2.3"
dependencies: