import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { TgChat, ChatType } from '../../database/entities/tg-chat.entity'; import { TdlibClientManagerService } from '../tdlib/tdlib-client-manager.service'; import { TgAccountsService } from '../tg-accounts/tg-accounts.service'; @Injectable() export class TgChatsService { constructor( @InjectRepository(TgChat) private chatRepo: Repository, private tdlibClientManager: TdlibClientManagerService, private tgAccountsService: TgAccountsService, ) {} async getChats(tenantId: string, accountId: string, limit = 50, offset = 0): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.chatRepo.find({ where: { accountId }, order: { lastMessageDate: 'DESC', updatedAt: 'DESC' }, take: limit, skip: offset, }); } async getChat(tenantId: string, accountId: string, tgChatId: number): Promise { await this.tgAccountsService.findById(tenantId, accountId); const chat = await this.chatRepo.findOne({ where: { accountId, tgChatId } }); if (chat) return chat; const live = await this.tdlibClientManager.sendRequest(accountId, { '@type': 'getChat', chat_id: tgChatId, }) as Record; return this.persistTdlibChat(accountId, live); } async getChatIdentity( tenantId: string, accountId: string, tgChatId: number, ): Promise> { const chat = await this.getChat(tenantId, accountId, tgChatId); const raw = (chat.raw || {}) as Record; const rawType = raw.type || {}; let user: Record | null = null; if (rawType['@type'] === 'chatTypePrivate' && rawType.user_id) { user = await this.tdlibClientManager.sendRequest(accountId, { '@type': 'getUser', user_id: Number(rawType.user_id), }) as Record; } const usernames = user?.usernames || {}; return { chatId: String(chat.tgChatId), title: user ? [user.first_name, user.last_name].filter(Boolean).join(' ') || chat.title : chat.title, username: usernames.editable_username || (Array.isArray(usernames.active_usernames) ? usernames.active_usernames[0] : '') || '', phone: user?.phone_number || '', userId: user?.id ? String(user.id) : undefined, photo: user?.profile_photo || chat.photo || null, chatType: chat.type, }; } async getChatHistory( tenantId: string, accountId: string, tgChatId: number, limit = 50, fromMessageId?: number, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); const request: Record = { '@type': 'getChatHistory', chat_id: tgChatId, limit, only_local: false, }; if (fromMessageId) { request.from_message_id = fromMessageId; } return this.tdlibClientManager.sendRequest(accountId, request); } async searchChats( tenantId: string, accountId: string, query: string, limit = 50, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'searchChats', query, limit, }); } async syncChatsFromTdlib(tenantId: string, accountId: string): Promise { await this.tgAccountsService.findById(tenantId, accountId); const result = await this.tdlibClientManager.sendRequest(accountId, { '@type': 'getChats', limit: 100, }) as Record; const chatIds = Array.isArray(result.chat_ids) ? result.chat_ids : []; const synced: TgChat[] = []; for (const chatId of chatIds) { try { const chat = await this.tdlibClientManager.sendRequest(accountId, { '@type': 'getChat', chat_id: Number(chatId), }) as Record; synced.push(await this.persistTdlibChat(accountId, chat)); } catch { // A deleted/inaccessible chat must not abort the remaining first sync. } } return { total: chatIds.length, synced: synced.length, chatIds }; } private async persistTdlibChat(accountId: string, chat: Record): Promise { const rawType = String(chat.type?.['@type'] || ''); const typeMap: Record = { chatTypePrivate: ChatType.PRIVATE, chatTypeBasicGroup: ChatType.GROUP, chatTypeSupergroup: ChatType.SUPERGROUP, chatTypeSecret: ChatType.SECRET, }; const persistedType = rawType === 'chatTypeSupergroup' && chat.type?.is_channel ? ChatType.CHANNEL : typeMap[rawType] || ChatType.PRIVATE; const content = chat.last_message?.content || {}; const lastText = content.text?.text || content.caption?.text || (content['@type'] ? `[${content['@type']}]` : ''); return this.upsertChat(accountId, { tgChatId: Number(chat.id), type: persistedType, title: String(chat.title || `Telegram ${chat.id}`), lastMessage: String(lastText || ''), lastMessageId: Number(chat.last_message?.id || 0), lastMessageDate: chat.last_message?.date ? new Date(Number(chat.last_message.date) * 1000) : undefined, unreadCount: Number(chat.unread_count || 0), photo: chat.photo, raw: chat, }); } async openChat(tenantId: string, accountId: string, tgChatId: number): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'openChat', chat_id: tgChatId, }); } async closeChat(tenantId: string, accountId: string, tgChatId: number): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'closeChat', chat_id: tgChatId, }); } async setArchived( tenantId: string, accountId: string, tgChatId: number, archived: boolean, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'addChatToList', chat_id: tgChatId, chat_list: { '@type': archived ? 'chatListArchive' : 'chatListMain' }, }); } async setPinned( tenantId: string, accountId: string, tgChatId: number, pinned: boolean, archived = false, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'toggleChatIsPinned', chat_list: { '@type': archived ? 'chatListArchive' : 'chatListMain' }, chat_id: tgChatId, is_pinned: pinned, }); } async setMute( tenantId: string, accountId: string, tgChatId: number, seconds: number, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'setChatNotificationSettings', chat_id: tgChatId, notification_settings: { '@type': 'chatNotificationSettings', use_mute_for: true, mute_for: Math.max(0, Math.floor(seconds)), }, }); } async setMarkedUnread( tenantId: string, accountId: string, tgChatId: number, marked: boolean, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'toggleChatIsMarkedAsUnread', chat_id: tgChatId, is_marked_as_unread: marked, }); } async deleteHistory( tenantId: string, accountId: string, tgChatId: number, options: { removeFromChatList?: boolean; revoke?: boolean }, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'deleteChatHistory', chat_id: tgChatId, remove_from_chat_list: options.removeFromChatList ?? false, revoke: options.revoke ?? false, }); } async leaveChat( tenantId: string, accountId: string, tgChatId: number, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'leaveChat', chat_id: tgChatId, }); } async getFolders(tenantId: string, accountId: string): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'getChatFolders', }); } async createFolder( tenantId: string, accountId: string, folder: Record, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'createChatFolder', folder, }); } async editFolder( tenantId: string, accountId: string, folderId: number, folder: Record, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'editChatFolder', chat_folder_id: folderId, folder, }); } async deleteFolder( tenantId: string, accountId: string, folderId: number, ): Promise { await this.tgAccountsService.findById(tenantId, accountId); return this.tdlibClientManager.sendRequest(accountId, { '@type': 'deleteChatFolder', chat_folder_id: folderId, leave_chat_ids: [], }); } async upsertChat(accountId: string, data: { tgChatId: number; type: ChatType; title: string; lastMessage?: string; lastMessageId?: number; lastMessageDate?: Date; unreadCount?: number; photo?: Record; raw?: Record; }): Promise { let chat = await this.chatRepo.findOne({ where: { accountId, tgChatId: data.tgChatId }, }); if (chat) { Object.assign(chat, data); } else { chat = this.chatRepo.create({ accountId, ...data }); } return this.chatRepo.save(chat); } }