feat(gateway): include production Telegram user gateway
Version the NestJS TDLib gateway with Docker deployment, migrations, automated tests and platform integration documentation while excluding all account sessions, credentials, runtime databases and backups.
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
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<TgChat>,
|
||||
private tdlibClientManager: TdlibClientManagerService,
|
||||
private tgAccountsService: TgAccountsService,
|
||||
) {}
|
||||
|
||||
async getChats(tenantId: string, accountId: string, limit = 50, offset = 0): Promise<TgChat[]> {
|
||||
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<TgChat> {
|
||||
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<string, any>;
|
||||
return this.persistTdlibChat(accountId, live);
|
||||
}
|
||||
|
||||
async getChatIdentity(
|
||||
tenantId: string,
|
||||
accountId: string,
|
||||
tgChatId: number,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const chat = await this.getChat(tenantId, accountId, tgChatId);
|
||||
const raw = (chat.raw || {}) as Record<string, any>;
|
||||
const rawType = raw.type || {};
|
||||
let user: Record<string, any> | 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<string, any>;
|
||||
}
|
||||
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<unknown> {
|
||||
await this.tgAccountsService.findById(tenantId, accountId);
|
||||
|
||||
const request: Record<string, unknown> = {
|
||||
'@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<unknown> {
|
||||
await this.tgAccountsService.findById(tenantId, accountId);
|
||||
|
||||
return this.tdlibClientManager.sendRequest(accountId, {
|
||||
'@type': 'searchChats',
|
||||
query,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
async syncChatsFromTdlib(tenantId: string, accountId: string): Promise<unknown> {
|
||||
await this.tgAccountsService.findById(tenantId, accountId);
|
||||
const result = await this.tdlibClientManager.sendRequest(accountId, {
|
||||
'@type': 'getChats',
|
||||
limit: 100,
|
||||
}) as Record<string, any>;
|
||||
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<string, any>;
|
||||
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<string, any>): Promise<TgChat> {
|
||||
const rawType = String(chat.type?.['@type'] || '');
|
||||
const typeMap: Record<string, ChatType> = {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
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<unknown> {
|
||||
await this.tgAccountsService.findById(tenantId, accountId);
|
||||
return this.tdlibClientManager.sendRequest(accountId, {
|
||||
'@type': 'leaveChat',
|
||||
chat_id: tgChatId,
|
||||
});
|
||||
}
|
||||
|
||||
async getFolders(tenantId: string, accountId: string): Promise<unknown> {
|
||||
await this.tgAccountsService.findById(tenantId, accountId);
|
||||
return this.tdlibClientManager.sendRequest(accountId, {
|
||||
'@type': 'getChatFolders',
|
||||
});
|
||||
}
|
||||
|
||||
async createFolder(
|
||||
tenantId: string,
|
||||
accountId: string,
|
||||
folder: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
await this.tgAccountsService.findById(tenantId, accountId);
|
||||
return this.tdlibClientManager.sendRequest(accountId, {
|
||||
'@type': 'createChatFolder',
|
||||
folder,
|
||||
});
|
||||
}
|
||||
|
||||
async editFolder(
|
||||
tenantId: string,
|
||||
accountId: string,
|
||||
folderId: number,
|
||||
folder: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
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<unknown> {
|
||||
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<string, unknown>;
|
||||
raw?: Record<string, unknown>;
|
||||
}): Promise<TgChat> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user