Q2TG/tools/import/importer.ts

255 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import TelegramChat from '../../src/client/TelegramChat';
import lottie from '../../src/constants/lottie';
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 名称(映射消息)' },
]));
let newChat: TelegramChat;
const { createNew } = await prompts({
type: 'confirm', name: 'createNew', message: '创建新的群组嘛',
});
if (createNew) {
const { chatName } = await prompts({
type: 'text', name: 'chatName', message: '请输入用于导入的群组名称(即将创建)',
});
newChat = await telegram.createChat(chatName);
}
else {
const { chatId } = await prompts({
type: 'number', name: 'chatId', message: '请输入用于导入的群组 ID数据库中必须有 accessHash',
});
newChat = await telegram.getChat(chatId);
}
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 = lottie.getTgsIndex(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`;
}
}
}
output += `${format(new Date, 'DD/MM/YYYY, HH:mm')} - 系统: 以上为导入的消息(你可以删除这条)\n`;
fetchFilesBar.stop();
// 转换好了,开始导入 TG
console.log('正在准备导入…');
const txtBuffer = Buffer.from(output, 'utf-8');
try {
const importSession = await newChat.startImportSession(
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();
console.log('导入成功!');
}
catch (e) {
console.error('错误', e);
const dumpPath = path.join(outputPath, 'record');
await fsP.writeFile(dumpPath, txtBuffer);
console.log('临时文件位置', outputPath);
}
},
};