|
|
@@ -0,0 +1,623 @@
|
|
|
+/*
|
|
|
+ * 聊天记录导出服务
|
|
|
+ * 用于导出Telegram聊天记录,包括文本和图片
|
|
|
+ */
|
|
|
+
|
|
|
+import {logger} from '../logger';
|
|
|
+import rootScope from '../rootScope';
|
|
|
+import pause from '../../helpers/schedulers/pause';
|
|
|
+
|
|
|
+export interface ChatRecordData {
|
|
|
+ id: string;
|
|
|
+ text: string;
|
|
|
+ mediaType: string;
|
|
|
+ date: string;
|
|
|
+ imagePath?: string | null;
|
|
|
+ isFromMe: boolean; // 是否是自己发送的消息
|
|
|
+ senderId?: string; // 发送者ID
|
|
|
+ senderName?: string; // 发送者名称
|
|
|
+}
|
|
|
+
|
|
|
+export interface DialogData {
|
|
|
+ name: string;
|
|
|
+ messages: ChatRecordData[];
|
|
|
+ error?: string;
|
|
|
+}
|
|
|
+
|
|
|
+export interface ExportData {
|
|
|
+ [dialogName: string]: DialogData;
|
|
|
+}
|
|
|
+
|
|
|
+export interface UploadResponse {
|
|
|
+ success: boolean;
|
|
|
+ message?: string;
|
|
|
+ data?: any;
|
|
|
+}
|
|
|
+
|
|
|
+export class ChatRecordsService {
|
|
|
+ private log = logger('[chat-records-service]');
|
|
|
+ private baseUrl: string = 'http://localhost:3010';
|
|
|
+ private retryCount: number = 3;
|
|
|
+ private retryDelay: number = 1000;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出所有聊天记录
|
|
|
+ */
|
|
|
+ public async exportAllChatRecords(): Promise<{zipBlob: Blob, fileName: string}> {
|
|
|
+ try {
|
|
|
+ // this.log('开始导出聊天记录...');
|
|
|
+
|
|
|
+ // 获取用户详细信息
|
|
|
+ const userDetails = await rootScope.managers.appUsersManager.getSelf();
|
|
|
+ if(!userDetails) {
|
|
|
+ throw new Error('无法获取用户信息');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取对话列表
|
|
|
+ const dialogs = await this.getDialogs();
|
|
|
+ // this.log('获取到对话数量:', dialogs.length);
|
|
|
+
|
|
|
+ // 限制对话数量,避免过多请求
|
|
|
+ const maxDialogs = 50;
|
|
|
+ const processedDialogs = dialogs.slice(0, maxDialogs);
|
|
|
+
|
|
|
+ // this.log(`开始导出,共处理 ${processedDialogs.length} 个对话`);
|
|
|
+
|
|
|
+ // 创建导出数据对象
|
|
|
+ const exportAllData: ExportData = {};
|
|
|
+ const processedPeerIds = new Set<string>();
|
|
|
+
|
|
|
+ // 图片文件列表
|
|
|
+ const imageFiles: {fileName: string, folderName: string, blob: Blob, peerId: string, messageId: number}[] = [];
|
|
|
+ let successCount = 0;
|
|
|
+ let failCount = 0;
|
|
|
+
|
|
|
+ // 设置批处理大小和延迟
|
|
|
+ const batchSize = 3;
|
|
|
+ const batchDelay = 1000;
|
|
|
+
|
|
|
+ // 按批次处理对话
|
|
|
+ for(let i = 0; i < processedDialogs.length; i += batchSize) {
|
|
|
+ const batchDialogs = processedDialogs.slice(i, i + batchSize);
|
|
|
+
|
|
|
+ // 串行处理每个对话
|
|
|
+ for(const dialog of batchDialogs) {
|
|
|
+ // this.log('跳过无效对话对象:', dialog);
|
|
|
+
|
|
|
+ // 从对话对象中提取peerId
|
|
|
+ let peerId;
|
|
|
+ try {
|
|
|
+ if(dialog.peer._ === 'peerUser' && 'user_id' in dialog.peer) {
|
|
|
+ const userId = dialog.peer.user_id;
|
|
|
+ peerId = userId;
|
|
|
+ } else if(dialog.peer._ === 'peerChat' && 'chat_id' in dialog.peer) {
|
|
|
+ const chatId = dialog.peer.chat_id;
|
|
|
+ peerId = -chatId;
|
|
|
+ } else if(dialog.peer._ === 'peerChannel' && 'channel_id' in dialog.peer) {
|
|
|
+ const channelId = dialog.peer.channel_id;
|
|
|
+ peerId = -channelId;
|
|
|
+ } else {
|
|
|
+ // this.log('未知的peer类型或缺少ID字段:', dialog.peer);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ } catch(e) {
|
|
|
+ // this.log('提取peerId失败:', e, '对话:', dialog);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(!peerId) {
|
|
|
+ // this.log('无法提取有效的peerId,跳过此对话');
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 跳过已处理的对话
|
|
|
+ if(processedPeerIds.has(peerId.toString())) {
|
|
|
+ // this.log(`跳过已处理的对话: ${peerId}`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ processedPeerIds.add(peerId.toString());
|
|
|
+
|
|
|
+ // 获取对话名称
|
|
|
+ let peerName = '';
|
|
|
+ if(await rootScope.managers.appPeersManager.isUser(peerId)) {
|
|
|
+ const user = await rootScope.managers.appUsersManager.getUser(peerId.toUserId());
|
|
|
+ peerName = user.first_name + (user.last_name ? ' ' + user.last_name : '');
|
|
|
+ } else if(await rootScope.managers.appPeersManager.isAnyChat(peerId)) {
|
|
|
+ const chat = await rootScope.managers.appChatsManager.getChat(peerId.toChatId());
|
|
|
+ peerName = chat.title;
|
|
|
+ }
|
|
|
+
|
|
|
+ // this.log(`正在处理对话: ${peerName}`);
|
|
|
+
|
|
|
+ // 获取所有聊天记录
|
|
|
+ const messages: any[] = [];
|
|
|
+ let downloadedImageCount = 0; // 当前对话已下载的图片数量
|
|
|
+ const maxImagesPerDialog = 30; // 每个对话最多下载30张图片
|
|
|
+ try {
|
|
|
+ // 分批获取历史消息
|
|
|
+ const messagesPerRequest = 50;
|
|
|
+ let offsetId = 0;
|
|
|
+ let hasMore = true;
|
|
|
+ const maxMessages = 500;
|
|
|
+
|
|
|
+ while(hasMore && messages.length < maxMessages) {
|
|
|
+ // 获取一批历史消息
|
|
|
+ const historyResult = await rootScope.managers.appMessagesManager.getHistory({
|
|
|
+ peerId: peerId,
|
|
|
+ limit: messagesPerRequest,
|
|
|
+ offsetId: offsetId,
|
|
|
+ addOffset: 0,
|
|
|
+ searchType: 'uncached'
|
|
|
+ });
|
|
|
+
|
|
|
+ // this.log(`获取到 ${historyResult?.messages?.length || 0} 条消息`);
|
|
|
+
|
|
|
+ if(historyResult && historyResult.messages && historyResult.messages.length) {
|
|
|
+ // 处理消息中的图片
|
|
|
+ for(const message of historyResult.messages) {
|
|
|
+ // 检查是否已达到图片下载限制
|
|
|
+ if(downloadedImageCount >= maxImagesPerDialog) {
|
|
|
+ // this.log(`对话 "${peerName}" 已达到图片下载限制 (${maxImagesPerDialog}张),跳过剩余图片`);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果消息包含图片,下载图片
|
|
|
+ if(message && 'media' in message && message.media && message.media._ === 'messageMediaPhoto' && message.media.photo) {
|
|
|
+ try {
|
|
|
+ const photo = message.media.photo;
|
|
|
+ const photoSizes = photo._ === 'photo' ? photo.sizes || [] : [];
|
|
|
+ const filteredSizes = [...photoSizes].filter(
|
|
|
+ (size): size is any => (size._ === 'photoSize' || size._ === 'photoSizeProgressive')
|
|
|
+ );
|
|
|
+ const photoSize = filteredSizes.sort((a, b) => (b.w * b.h) - (a.w * a.h))[0];
|
|
|
+
|
|
|
+ if(photoSize) {
|
|
|
+ try {
|
|
|
+ // 使用API直接下载图片
|
|
|
+ const blob = await this.downloadImageBlob(photo, photoSize);
|
|
|
+
|
|
|
+ if(blob) {
|
|
|
+ // 使用与UI导出相同的文件名格式
|
|
|
+ const fileName = `${peerId}_${message.id}_${message.date}_${photo.id}.jpg`;
|
|
|
+ const folderName = `photos`;
|
|
|
+
|
|
|
+ // 添加到图片文件列表
|
|
|
+ imageFiles.push({
|
|
|
+ fileName,
|
|
|
+ folderName,
|
|
|
+ blob,
|
|
|
+ peerId: peerId.toString(),
|
|
|
+ messageId: message.id
|
|
|
+ });
|
|
|
+
|
|
|
+ // 在消息中添加图片引用
|
|
|
+ (message as any).localImagePath = `${folderName}/${fileName}`;
|
|
|
+ // this.log(`成功下载图片: ${fileName}`);
|
|
|
+ successCount++;
|
|
|
+ downloadedImageCount++; // 增加当前对话的图片计数
|
|
|
+ } else {
|
|
|
+ (message as any).localImagePath = null;
|
|
|
+ failCount++;
|
|
|
+ }
|
|
|
+ } catch(e) {
|
|
|
+ // this.log('下载图片失败:', e);
|
|
|
+ (message as any).localImagePath = null;
|
|
|
+ failCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch(e) {
|
|
|
+ // this.log('处理图片失败:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加消息到结果中
|
|
|
+ messages.push(...historyResult.messages);
|
|
|
+
|
|
|
+ // 更新offsetId为最后一条消息的ID
|
|
|
+ const lastMessage = historyResult.messages[historyResult.messages.length - 1];
|
|
|
+ offsetId = lastMessage.mid;
|
|
|
+
|
|
|
+ // 检查是否还有更多消息
|
|
|
+ hasMore = historyResult.messages.length === messagesPerRequest;
|
|
|
+ } else {
|
|
|
+ // this.log(`对话 "${peerName}" 没有更多消息,停止获取`);
|
|
|
+ hasMore = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 每次请求后稍微暂停一下
|
|
|
+ await pause(300);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理消息,提取有用信息
|
|
|
+ const processedMessages = await this.processMessages(messages, peerId.toString());
|
|
|
+
|
|
|
+ // 将该对话的消息添加到导出数据
|
|
|
+ if(!exportAllData[peerId.toString()]) {
|
|
|
+ exportAllData[peerId.toString()] = {
|
|
|
+ name: peerName,
|
|
|
+ messages: []
|
|
|
+ };
|
|
|
+ }
|
|
|
+ exportAllData[peerId.toString()].messages.push(...processedMessages);
|
|
|
+
|
|
|
+ // this.log(`已获取对话 "${peerName}" 的 ${messages.length} 条消息,处理后数量: ${processedMessages.length},下载图片: ${downloadedImageCount}张`);
|
|
|
+ } catch(e) {
|
|
|
+ // this.log(`获取对话 ${peerName} 的历史消息失败:`, e);
|
|
|
+ exportAllData[peerId.toString()] = {
|
|
|
+ name: peerName,
|
|
|
+ messages: [],
|
|
|
+ error: '获取消息失败'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 每次请求后稍微暂停一下
|
|
|
+ await pause(500);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 每批处理后暂停一下
|
|
|
+ if(i + batchSize < processedDialogs.length) {
|
|
|
+ // this.log(`完成批次 ${i/batchSize + 1}/${Math.ceil(processedDialogs.length/batchSize)},暂停中...`);
|
|
|
+ await pause(batchDelay);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // this.log('===== 导出所有对话的聊天记录 =====');
|
|
|
+ // this.log(`共导出 ${Object.keys(exportAllData).length} 个对话`);
|
|
|
+ // this.log(`图片下载统计: 成功 ${successCount} 张,失败 ${failCount} 张,总计 ${imageFiles.length} 张`);
|
|
|
+
|
|
|
+ // 创建ZIP文件
|
|
|
+ const zipBlob = await this.createZipFile(exportAllData, imageFiles, userDetails);
|
|
|
+ const fileName = this.generateFileName(userDetails);
|
|
|
+
|
|
|
+ return {zipBlob, fileName};
|
|
|
+ } catch(error) {
|
|
|
+ // this.log('导出聊天记录时出错:', error);
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理消息
|
|
|
+ */
|
|
|
+ private async processMessages(messages: any[], peerId: string): Promise<ChatRecordData[]> {
|
|
|
+ const currentUserId = rootScope.myId;
|
|
|
+ const processedMessages: ChatRecordData[] = [];
|
|
|
+
|
|
|
+ for(const message of messages) {
|
|
|
+ try {
|
|
|
+ let textContent = '';
|
|
|
+ let mediaType = 'None';
|
|
|
+ let date = '';
|
|
|
+ let isFromMe = false;
|
|
|
+ let senderId = '';
|
|
|
+ let senderName = '';
|
|
|
+ const imagePath = (message as any).localImagePath || null;
|
|
|
+
|
|
|
+ if(message) {
|
|
|
+ // 判断是否是自己发送的消息
|
|
|
+ let fromPeerId = null;
|
|
|
+
|
|
|
+ // 检查各种可能的发送者字段
|
|
|
+ if('from_id' in message && message.from_id) {
|
|
|
+ // 处理from_id可能是对象的情况
|
|
|
+ if(typeof message.from_id === 'object' && message.from_id) {
|
|
|
+ if('user_id' in message.from_id) {
|
|
|
+ fromPeerId = message.from_id.user_id;
|
|
|
+ } else if('id' in message.from_id) {
|
|
|
+ fromPeerId = message.from_id.id;
|
|
|
+ } else if('channel_id' in message.from_id) {
|
|
|
+ fromPeerId = message.from_id.channel_id;
|
|
|
+ } else if('chat_id' in message.from_id) {
|
|
|
+ fromPeerId = message.from_id.chat_id;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ fromPeerId = message.from_id;
|
|
|
+ }
|
|
|
+ } else if('fromId' in message && message.fromId) {
|
|
|
+ fromPeerId = message.fromId;
|
|
|
+ } else if('from' in message && message.from) {
|
|
|
+ if(typeof message.from === 'object' && message.from.id) {
|
|
|
+ fromPeerId = message.from.id;
|
|
|
+ }
|
|
|
+ } else if('peer_id' in message && message.peer_id) {
|
|
|
+ fromPeerId = message.peer_id;
|
|
|
+ } else if('sender_id' in message && message.sender_id) {
|
|
|
+ fromPeerId = message.sender_id;
|
|
|
+ } else if('senderId' in message && message.senderId) {
|
|
|
+ fromPeerId = message.senderId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(fromPeerId) {
|
|
|
+ // 尝试不同的ID比较方法
|
|
|
+ const fromPeerIdStr = fromPeerId.toString();
|
|
|
+ const currentUserIdStr = currentUserId.toString();
|
|
|
+
|
|
|
+ // 直接比较
|
|
|
+ isFromMe = fromPeerId === currentUserId;
|
|
|
+
|
|
|
+ // 如果直接比较失败,尝试字符串比较
|
|
|
+ if(!isFromMe) {
|
|
|
+ isFromMe = fromPeerIdStr === currentUserIdStr;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果还是失败,尝试转换为数字比较
|
|
|
+ if(!isFromMe) {
|
|
|
+ const fromPeerIdNum = parseInt(fromPeerIdStr);
|
|
|
+ const currentUserIdNum = parseInt(currentUserIdStr);
|
|
|
+ if(!isNaN(fromPeerIdNum) && !isNaN(currentUserIdNum)) {
|
|
|
+ isFromMe = fromPeerIdNum === currentUserIdNum;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ senderId = fromPeerIdStr;
|
|
|
+
|
|
|
+ // 获取发送者名称
|
|
|
+ if(isFromMe) {
|
|
|
+ senderName = '我';
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ if(await rootScope.managers.appPeersManager.isUser(fromPeerId)) {
|
|
|
+ const user = await rootScope.managers.appUsersManager.getUser(fromPeerId.toUserId());
|
|
|
+ if(user) {
|
|
|
+ const firstName = user.first_name || '';
|
|
|
+ const lastName = user.last_name || '';
|
|
|
+ const tgName = `${firstName} ${lastName}`.trim();
|
|
|
+
|
|
|
+ if(user.username) {
|
|
|
+ senderName = `@${user.username}`;
|
|
|
+ } else if(tgName) {
|
|
|
+ senderName = tgName;
|
|
|
+ } else {
|
|
|
+ senderName = '未知用户';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if(await rootScope.managers.appPeersManager.isAnyChat(fromPeerId)) {
|
|
|
+ const chat = await rootScope.managers.appChatsManager.getChat(fromPeerId.toChatId());
|
|
|
+ senderName = chat?.title || '未知群组';
|
|
|
+ }
|
|
|
+ } catch(error) {
|
|
|
+ // this.log('获取发送者名称失败:', error);
|
|
|
+ senderName = '未知用户';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ senderName = '系统';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取文本内容
|
|
|
+ if('message' in message && message.message) {
|
|
|
+ textContent = message.message;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取媒体类型
|
|
|
+ if('media' in message && message.media) {
|
|
|
+ mediaType = message.media._.replace('messageMedia', '');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取日期
|
|
|
+ if(message.date) {
|
|
|
+ date = new Date(message.date * 1000).toLocaleString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ processedMessages.push({
|
|
|
+ id: (message.mid || message.id).toString(),
|
|
|
+ text: textContent,
|
|
|
+ mediaType: mediaType,
|
|
|
+ date: date,
|
|
|
+ imagePath: imagePath,
|
|
|
+ isFromMe: isFromMe,
|
|
|
+ senderId: senderId,
|
|
|
+ senderName: senderName
|
|
|
+ });
|
|
|
+ } catch(error) {
|
|
|
+ // this.log('处理单条消息时出错:', error);
|
|
|
+ processedMessages.push({
|
|
|
+ id: (message.mid || message.id || 'unknown').toString(),
|
|
|
+ text: '[消息处理失败]',
|
|
|
+ mediaType: 'Error',
|
|
|
+ date: new Date().toLocaleString(),
|
|
|
+ imagePath: null,
|
|
|
+ isFromMe: false,
|
|
|
+ senderId: '',
|
|
|
+ senderName: '系统'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return processedMessages;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载图片Blob
|
|
|
+ */
|
|
|
+ private async downloadImageBlob(photo: any, photoSize: any): Promise<Blob | null> {
|
|
|
+ try {
|
|
|
+ // 只使用成功的方式:512KB limit
|
|
|
+ const fileResult = await rootScope.managers.apiManager.invokeApi('upload.getFile', {
|
|
|
+ location: {
|
|
|
+ _: 'inputPhotoFileLocation',
|
|
|
+ id: photo.id,
|
|
|
+ access_hash: photo.access_hash,
|
|
|
+ file_reference: photo.file_reference,
|
|
|
+ thumb_size: photoSize.type
|
|
|
+ },
|
|
|
+ offset: 0,
|
|
|
+ limit: 512 * 1024 // 512KB limit
|
|
|
+ });
|
|
|
+
|
|
|
+ if(fileResult && (fileResult as any).bytes) {
|
|
|
+ const blob = new Blob([(fileResult as any).bytes], {type: 'image/jpeg'});
|
|
|
+ return blob;
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ } catch(error) {
|
|
|
+ // this.log('下载图片失败:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传聊天记录文件
|
|
|
+ */
|
|
|
+ public async uploadChatRecords(zipBlob: Blob, fileName: string, fishId: string, description?: string): Promise<UploadResponse> {
|
|
|
+ const url = `${this.baseUrl}/api/records/upload`;
|
|
|
+
|
|
|
+ for(let attempt = 1; attempt <= this.retryCount; attempt++) {
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', zipBlob, fileName);
|
|
|
+ formData.append('fishId', fishId);
|
|
|
+ if(description) {
|
|
|
+ formData.append('description', description);
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: 'POST',
|
|
|
+ body: formData
|
|
|
+ });
|
|
|
+
|
|
|
+ if(response.ok) {
|
|
|
+ const result = await response.json();
|
|
|
+ // this.log('成功上传聊天记录:', result);
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ data: result
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ const errorText = await response.text();
|
|
|
+ if(attempt < this.retryCount) {
|
|
|
+ await this.delay(this.retryDelay);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // this.log('上传聊天记录失败:', {
|
|
|
+ // status: response.status,
|
|
|
+ // error: errorText
|
|
|
+ // });
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: `HTTP错误 ${response.status}: ${errorText}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch(error) {
|
|
|
+ if(attempt < this.retryCount) {
|
|
|
+ await this.delay(this.retryDelay);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // this.log('上传聊天记录时出错:', error);
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: error instanceof Error ? error.message : '未知错误'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: '所有重试尝试都失败了'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取对话列表
|
|
|
+ */
|
|
|
+ private async getDialogs(): Promise<any[]> {
|
|
|
+ try {
|
|
|
+ const result = await rootScope.managers.apiManager.invokeApi('messages.getDialogs', {
|
|
|
+ offset_date: 0,
|
|
|
+ offset_id: 0,
|
|
|
+ offset_peer: {_: 'inputPeerEmpty'},
|
|
|
+ limit: 100,
|
|
|
+ hash: '0'
|
|
|
+ });
|
|
|
+
|
|
|
+ if(result._ === 'messages.dialogs' || result._ === 'messages.dialogsSlice') {
|
|
|
+ await rootScope.managers.appMessagesManager.saveMessages(result.messages);
|
|
|
+ await rootScope.managers.appChatsManager.saveApiChats(result.chats);
|
|
|
+ await rootScope.managers.appUsersManager.saveApiUsers(result.users);
|
|
|
+ return result.dialogs;
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ } catch(error) {
|
|
|
+ // this.log('获取对话列表失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建ZIP文件
|
|
|
+ */
|
|
|
+ private async createZipFile(exportData: ExportData, imageFiles: {fileName: string, folderName: string, blob: Blob, peerId: string, messageId: number}[], userDetails: any): Promise<Blob> {
|
|
|
+ // 动态加载JSZip库
|
|
|
+ if(!(window as any).JSZip) {
|
|
|
+ await this.loadJSZip();
|
|
|
+ }
|
|
|
+
|
|
|
+ const zip = new (window as any).JSZip();
|
|
|
+
|
|
|
+ // 添加图片文件到ZIP
|
|
|
+ for(const imageFile of imageFiles) {
|
|
|
+ zip.file(`${imageFile.folderName}/${imageFile.fileName}`, imageFile.blob);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加聊天记录JSON文件
|
|
|
+ const jsonString = JSON.stringify(exportData, null, 2);
|
|
|
+ zip.file('telegram_export.json', jsonString);
|
|
|
+
|
|
|
+ // 添加导出信息文件
|
|
|
+ const exportInfo = {
|
|
|
+ version: '1.0',
|
|
|
+ date: new Date().toISOString(),
|
|
|
+ user: {
|
|
|
+ id: userDetails.id,
|
|
|
+ firstName: userDetails.first_name,
|
|
|
+ lastName: userDetails.last_name || '',
|
|
|
+ username: userDetails.username || ''
|
|
|
+ },
|
|
|
+ stats: {
|
|
|
+ dialogs: Object.keys(exportData).length,
|
|
|
+ images: imageFiles.length
|
|
|
+ }
|
|
|
+ };
|
|
|
+ zip.file('export_info.json', JSON.stringify(exportInfo, null, 2));
|
|
|
+
|
|
|
+ return zip.generateAsync({type: 'blob'});
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 动态加载JSZip库
|
|
|
+ */
|
|
|
+ private async loadJSZip(): Promise<void> {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const script = document.createElement('script');
|
|
|
+ script.src = 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js';
|
|
|
+ script.onload = () => resolve();
|
|
|
+ script.onerror = () => reject(new Error('无法加载JSZip库'));
|
|
|
+ document.head.appendChild(script);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成文件名
|
|
|
+ */
|
|
|
+ private generateFileName(userDetails: any): string {
|
|
|
+ const userName = userDetails.first_name + (userDetails.last_name ? '_' + userDetails.last_name : '');
|
|
|
+ const now = new Date();
|
|
|
+ const timeString = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}-${String(now.getMinutes()).padStart(2, '0')}`;
|
|
|
+ return `telegram_export_${userName}_${timeString}.zip`;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 延迟函数
|
|
|
+ */
|
|
|
+ private delay(ms: number): Promise<void> {
|
|
|
+ return new Promise(resolve => setTimeout(resolve, ms));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export const chatRecordsService = new ChatRecordsService();
|