@2byte/tgbot-framework 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +300 -300
  2. package/bin/2byte-cli.ts +97 -97
  3. package/package.json +6 -5
  4. package/src/cli/CreateBotCommand.ts +181 -181
  5. package/src/cli/GenerateCommand.ts +195 -195
  6. package/src/cli/InitCommand.ts +107 -107
  7. package/src/cli/TgAccountManager.ts +50 -0
  8. package/src/console/migrate.ts +82 -82
  9. package/src/core/ApiService.ts +20 -20
  10. package/src/core/ApiServiceManager.ts +63 -63
  11. package/src/core/App.ts +1143 -1113
  12. package/src/core/BotArtisan.ts +79 -79
  13. package/src/core/BotMigration.ts +30 -30
  14. package/src/core/BotSeeder.ts +66 -66
  15. package/src/core/Model.ts +84 -84
  16. package/src/core/utils.ts +2 -2
  17. package/src/illumination/Artisan.ts +149 -149
  18. package/src/illumination/InlineKeyboard.ts +61 -61
  19. package/src/illumination/Message2Byte.ts +255 -255
  20. package/src/illumination/Message2ByteLiveProgressive.ts +278 -278
  21. package/src/illumination/Message2bytePool.ts +107 -107
  22. package/src/illumination/Migration.ts +186 -186
  23. package/src/illumination/RunSectionRoute.ts +85 -85
  24. package/src/illumination/Section.ts +410 -410
  25. package/src/illumination/SectionComponent.ts +64 -64
  26. package/src/illumination/Telegraf2byteContext.ts +32 -32
  27. package/src/index.ts +42 -35
  28. package/src/libs/TelegramAccountControl.ts +1140 -738
  29. package/src/libs/TgSender.ts +53 -0
  30. package/src/models/Model.ts +67 -0
  31. package/src/models/Proxy.ts +218 -0
  32. package/src/models/TgAccount.ts +362 -0
  33. package/src/models/index.ts +3 -0
  34. package/src/types.ts +191 -188
  35. package/src/user/UserModel.ts +297 -297
  36. package/src/user/UserStore.ts +119 -119
  37. package/src/workflow/services/MassSendApiService.ts +80 -80
  38. package/templates/bot/.env.example +23 -19
  39. package/templates/bot/artisan.ts +8 -8
  40. package/templates/bot/bot.ts +82 -79
  41. package/templates/bot/database/dbConnector.ts +4 -4
  42. package/templates/bot/database/migrate.ts +9 -9
  43. package/templates/bot/database/migrations/001_create_users.sql +18 -18
  44. package/templates/bot/database/migrations/007_proxy.sql +27 -0
  45. package/templates/bot/database/migrations/008_tg_accounts.sql +32 -0
  46. package/templates/bot/database/seed.ts +14 -14
  47. package/templates/bot/docs/CLI_SERVICES.md +536 -0
  48. package/templates/bot/docs/INPUT_SYSTEM.md +211 -0
  49. package/templates/bot/docs/SERVICE_EXAMPLES.md +384 -0
  50. package/templates/bot/docs/TASK_SYSTEM.md +156 -0
  51. package/templates/bot/models/Model.ts +7 -0
  52. package/templates/bot/models/index.ts +2 -0
  53. package/templates/bot/package.json +30 -30
  54. package/templates/bot/sectionList.ts +9 -9
  55. package/templates/bot/sections/ExampleInputSection.ts +85 -85
  56. package/templates/bot/sections/ExampleLiveTaskerSection.ts +60 -60
  57. package/templates/bot/sections/HomeSection.ts +63 -63
  58. package/templates/bot/workflow/services/{ExampleServise.ts → ExampleService.ts} +23 -23
@@ -1,738 +1,1140 @@
1
- import { TelegramClient, Api } from "telegram";
2
- import { StringSession } from "telegram/sessions";
3
- import fs from "fs";
4
- import { TelegramClientParams } from "telegram/client/telegramBaseClient";
5
- import { ProxyInterface } from "telegram/network/connection/TCPMTProxy";
6
-
7
- interface TelegramRegistrarInit {
8
- appId: string;
9
- appHash: string;
10
- credetialsManager: TelegramManagerCredentials;
11
- }
12
-
13
- type TelegramClientProxy = {
14
- ip: string;
15
- port: number;
16
- username?: string;
17
- password?: string;
18
- secret?: string,
19
- socksType?: 5 | 4;
20
- MTProxy?: boolean;
21
- } & ProxyInterface;
22
-
23
- interface TelegramInitClient {
24
- phone: string;
25
- proxy?: any;
26
- }
27
-
28
- export interface TelegramCredentials {
29
- phone: string;
30
- proxy?: TelegramClientProxy;
31
- password?: string;
32
- session?: string;
33
- }
34
-
35
- interface TelegramCredentialsManagerInit {
36
- pathFileStorage: string;
37
- pathFileProxyList: string;
38
- pathFileCounterOffset: string;
39
- }
40
-
41
- export class TelegramManagerCredentials {
42
- private requiredProxyForCredentials: boolean = false;
43
- private credentials: TelegramCredentials[] = [];
44
- private proxyList: string[] = [];
45
-
46
- static init(options: TelegramCredentialsManagerInit) {
47
- return new TelegramManagerCredentials(options);
48
- }
49
-
50
- constructor(private options: TelegramCredentialsManagerInit) {
51
- this.ensureStorageDirectory();
52
- this.loadCredentials();
53
- this.loadProxyList();
54
- }
55
-
56
- /**
57
- * Ensure storage directory exists
58
- */
59
- private ensureStorageDirectory(): void {
60
- const storageDir = this.options.pathFileStorage.split("/").slice(0, -1).join("/");
61
- if (!fs.existsSync(storageDir)) {
62
- fs.mkdirSync(storageDir, { recursive: true });
63
- }
64
- }
65
-
66
- /**
67
- * Load credentials from JSON file
68
- */
69
- private loadCredentials(): void {
70
- try {
71
- if (fs.existsSync(this.options.pathFileStorage)) {
72
- const data = fs.readFileSync(this.options.pathFileStorage, "utf8");
73
- this.credentials = JSON.parse(data) || [];
74
- } else {
75
- this.credentials = [];
76
- this.saveCredentials();
77
- }
78
- } catch (error) {
79
- console.error("Error loading credentials:", error);
80
- this.credentials = [];
81
- }
82
- }
83
-
84
- /**
85
- * Загрузить список прокси из файла
86
- */
87
- private loadProxyList(): void {
88
- try {
89
- if (fs.existsSync(this.options.pathFileProxyList)) {
90
- const data = fs.readFileSync(this.options.pathFileProxyList, "utf8");
91
- this.proxyList = data.split('\n').filter(line => line.trim() !== '');
92
- } else {
93
- this.proxyList = [];
94
- }
95
- } catch (error) {
96
- console.error("Ошибка загрузки списка прокси:", error);
97
- this.proxyList = [];
98
- }
99
- }
100
-
101
- /**
102
- * Получить текущий offset из файла счетчика
103
- */
104
- private getCurrentOffset(): number {
105
- try {
106
- if (fs.existsSync(this.options.pathFileCounterOffset)) {
107
- const data = fs.readFileSync(this.options.pathFileCounterOffset, "utf8");
108
- return parseInt(data.trim()) || 0;
109
- }
110
- return 0;
111
- } catch (error) {
112
- console.error("Ошибка чтения offset:", error);
113
- return 0;
114
- }
115
- }
116
-
117
- /**
118
- * Сохранить новый offset в файл счетчика
119
- */
120
- private saveOffset(offset: number): void {
121
- try {
122
- fs.writeFileSync(this.options.pathFileCounterOffset, offset.toString(), "utf8");
123
- } catch (error) {
124
- console.error("Ошибка сохранения offset:", error);
125
- throw new Error("Не удалось сохранить offset");
126
- }
127
- }
128
-
129
- /**
130
- * Парсинг строки прокси в формат TelegramClientProxy
131
- * Поддерживаемые форматы:
132
- * - socks4://username:password@host:port
133
- * - socks5://username:password@host:port
134
- * - http://username:password@host:port (будет использоваться как SOCKS5)
135
- */
136
- private parseProxyString(proxyString: string): TelegramClientProxy {
137
- // Проверяем SOCKS4 формат
138
- let match = proxyString.match(/^socks4:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/);
139
- if (match) {
140
- const [, username, password, ip, port] = match;
141
- return {
142
- ip,
143
- port: parseInt(port),
144
- username,
145
- password,
146
- socksType: 4
147
- };
148
- }
149
-
150
- // Проверяем SOCKS5 формат
151
- match = proxyString.match(/^socks5:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/);
152
- if (match) {
153
- const [, username, password, ip, port] = match;
154
- return {
155
- ip,
156
- port: parseInt(port),
157
- username,
158
- password,
159
- socksType: 5
160
- };
161
- }
162
-
163
- // Проверяем HTTP формат (используем как SOCKS5)
164
- match = proxyString.match(/^http:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/);
165
- if (match) {
166
- const [, username, password, ip, port] = match;
167
- return {
168
- ip,
169
- port: parseInt(port),
170
- username,
171
- password,
172
- socksType: 5 // HTTP прокси используем как SOCKS5
173
- };
174
- }
175
-
176
- throw new Error(`Неверный формат прокси: ${proxyString}. Поддерживаемые форматы: socks4://, socks5://, http://`);
177
- }
178
-
179
- /**
180
- * Получить следующий прокси из списка
181
- */
182
- private getNextProxy(): TelegramClientProxy {
183
- if (this.proxyList.length === 0) {
184
- throw new Error("Список прокси пуст");
185
- }
186
-
187
- const currentOffset = this.getCurrentOffset();
188
-
189
- if (currentOffset >= this.proxyList.length) {
190
- throw new Error("Прокси закончились");
191
- }
192
-
193
- const proxyString = this.proxyList[currentOffset];
194
- const proxy = this.parseProxyString(proxyString);
195
-
196
- // Увеличиваем offset для следующего использования
197
- this.saveOffset(currentOffset + 1);
198
-
199
- return proxy;
200
- }
201
-
202
- /**
203
- * Save credentials to JSON file
204
- */
205
- private saveCredentials(): void {
206
- try {
207
- fs.writeFileSync(
208
- this.options.pathFileStorage,
209
- JSON.stringify(this.credentials, null, 2),
210
- "utf8"
211
- );
212
- } catch (error) {
213
- console.error("Error saving credentials:", error);
214
- throw new Error("Failed to save credentials");
215
- }
216
- }
217
-
218
- /**
219
- * Add or update credential
220
- */
221
- addCredential(credential: TelegramCredentials): void {
222
-
223
- // Attach proxy to credential
224
- if (this.requiredProxyForCredentials && !credential.proxy) {
225
- try {
226
- credential.proxy = this.getNextProxy();
227
- } catch (error) {
228
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
229
- throw new Error(`Не удалось назначить прокси: ${errorMessage}`);
230
- }
231
- }
232
-
233
- const existingIndex = this.credentials.findIndex((c) => c != null && c.phone === credential.phone);
234
-
235
- if (existingIndex !== -1) {
236
- // Update existing credential
237
- this.credentials[existingIndex] = { ...this.credentials[existingIndex], ...credential };
238
- } else {
239
- // Add new credential
240
- this.credentials.push(credential);
241
- }
242
-
243
- this.credentials = this.credentials.filter((c) => c != null);
244
-
245
- this.saveCredentials();
246
- }
247
-
248
- /**
249
- * Get credential by phone number
250
- */
251
- getCredential(phone: string): TelegramCredentials | null {
252
- return this.credentials.find((c) => c != null && c.phone === phone) || null;
253
- }
254
-
255
- /**
256
- * Delete credential by phone number
257
- */
258
- deleteCredential(phone: string): boolean {
259
- const initialLength = this.credentials.length;
260
- this.credentials = this.credentials.filter((c) => c.phone !== phone);
261
-
262
- if (this.credentials.length < initialLength) {
263
- this.saveCredentials();
264
- return true;
265
- }
266
-
267
- return false;
268
- }
269
-
270
- /**
271
- * Get all credentials
272
- */
273
- getAllCredentials(): TelegramCredentials[] {
274
- return [...this.credentials];
275
- }
276
-
277
- /**
278
- * Check if credential exists
279
- */
280
- hasCredential(phone: string): boolean {
281
- return this.credentials.some((c) => c.phone === phone);
282
- }
283
-
284
- /**
285
- * Update session for existing credential
286
- */
287
- updateSession(phone: string, session: string): boolean {
288
- const credential = this.credentials.find((c) => c.phone === phone);
289
- if (credential) {
290
- credential.session = session;
291
- this.saveCredentials();
292
- return true;
293
- }
294
- return false;
295
- }
296
-
297
- /**
298
- * Get credentials count
299
- */
300
- getCredentialsCount(): number {
301
- return this.credentials.length;
302
- }
303
-
304
- /**
305
- * Clear all credentials
306
- */
307
- clearAllCredentials(): void {
308
- this.credentials = [];
309
- this.saveCredentials();
310
- }
311
-
312
- /**
313
- * Get credentials by proxy configuration
314
- */
315
- getCredentialsByProxy(proxyHost: string, proxyPort: number): TelegramCredentials[] {
316
- return this.credentials.filter(
317
- (c) => c.proxy && c.proxy.ip === proxyHost && c.proxy.port === proxyPort
318
- );
319
- }
320
-
321
- /**
322
- * Get credentials without proxy
323
- */
324
- getCredentialsWithoutProxy(): TelegramCredentials[] {
325
- return this.credentials.filter((c) => !c.proxy);
326
- }
327
- }
328
-
329
- export class TelegramAccountRemote {
330
- private initOptions: TelegramRegistrarInit;
331
- private tgClient!: TelegramClient; // Используем definite assignment assertion
332
- private credentialsManager: TelegramManagerCredentials;
333
-
334
- static init(initOptions: TelegramRegistrarInit) {
335
- return new TelegramAccountRemote(initOptions);
336
- }
337
-
338
- constructor(initOptions: TelegramRegistrarInit) {
339
- this.initOptions = initOptions;
340
- this.credentialsManager = initOptions.credetialsManager;
341
- }
342
-
343
- async attemptRestoreSession(credentials: TelegramCredentials): Promise<boolean> {
344
- const { phone, proxy, session, password } = credentials;
345
- const { appId, appHash } = this.initOptions;
346
-
347
- if (session) {
348
- const clientParams: TelegramClientParams = {connectionRetries: 5};
349
-
350
- if (credentials.proxy) {
351
- clientParams.proxy = credentials.proxy;
352
- }
353
-
354
- this.tgClient = new TelegramClient(new StringSession(session), Number(appId), appHash, clientParams);
355
-
356
- return await this.tgClient.connect();
357
- }
358
-
359
- return false;
360
- }
361
-
362
- async login(
363
- credentials: TelegramCredentials,
364
- cbRequestCode: CallableFunction,
365
- cbRequestPassword: CallableFunction,
366
- cbError: CallableFunction,
367
- ): Promise<void> {
368
- const { phone, proxy, session, password } = credentials;
369
- const { appId, appHash } = this.initOptions;
370
-
371
- if (session) {
372
- throw new Error("Account is already registered");
373
- }
374
-
375
- try {
376
- this.tgClient = new TelegramClient(new StringSession(""), Number(appId), appHash, {
377
- connectionRetries: 5,
378
- proxy: credentials.proxy,
379
- timeout: 10000 * 60 * 10,
380
- });
381
- console.log('Login cresentials:', credentials)
382
- // @ts-ignore
383
- await this.tgClient.start({
384
- phoneNumber: async () => phone,
385
- password: async () => {
386
- if (!password) {
387
- return await cbRequestPassword();
388
- }
389
- return password;
390
- },
391
- phoneCode: async () => {
392
- return await cbRequestCode();
393
- },
394
- onError: (err: Error) => {
395
- cbError('Error for wait authentiction', err);
396
- },
397
- });
398
-
399
- const session = this.tgClient.session.save() as unknown as string;
400
-
401
- this.credentialsManager.addCredential({
402
- phone,
403
- proxy,
404
- session,
405
- password,
406
- });
407
-
408
- console.log("Регистрация успешна!");
409
- console.log("Сессия:\n", session);
410
-
411
- } catch (err) {
412
- throw new Error("Failed to register account", { cause: err });
413
- }
414
- }
415
-
416
- async disconnect(): Promise<void> {
417
- await this.tgClient.disconnect();
418
- }
419
-
420
- async disconnectWhenConnect() {
421
- await this.tgClient.disconnect();
422
- }
423
-
424
- /**
425
- * Отправляет команду /start боту
426
- * @param botUsername username бота (например: 'my_bot' или '@my_bot')
427
- * @returns true если команда успешно отправлена
428
- */
429
- async sendStartCommand(botUsername: string): Promise<boolean> {
430
- if (!this.tgClient) {
431
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
432
- }
433
-
434
- try {
435
- // Убираем @ из username если он есть
436
- const normalizedUsername = botUsername.startsWith('@') ? botUsername.substring(1) : botUsername;
437
-
438
- // Ищем бота по username
439
- const result = await this.tgClient.sendMessage(normalizedUsername, {
440
- message: '/start'
441
- });
442
-
443
- return result ? true : false;
444
- } catch (error) {
445
- console.error('Error sending /start command:', error);
446
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
447
- throw new Error(`Failed to send /start command to @${botUsername}: ${errorMessage}`);
448
- }
449
- }
450
-
451
- /**
452
- * Получает ID чата с ботом
453
- * @param botUsername username бота (например: 'my_bot' или '@my_bot')
454
- * @returns ID чата с ботом
455
- */
456
- async getBotChatId(botUsername: string): Promise<number> {
457
- if (!this.tgClient) {
458
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
459
- }
460
-
461
- try {
462
- // Убираем @ из username если он есть
463
- const normalizedUsername = botUsername.startsWith('@') ? botUsername.substring(1) : botUsername;
464
-
465
- // Получаем информацию о диалоге с ботом
466
- const dialog = await this.tgClient.getDialogs({
467
- limit: 1
468
- });
469
-
470
- if (!dialog || dialog.length === 0 || !dialog[0] || !dialog[0].id) {
471
- throw new Error(`Chat with bot @${normalizedUsername} not found`);
472
- }
473
-
474
- return dialog[0].id!.toJSNumber();
475
- } catch (error) {
476
- console.error('Error getting bot chat ID:', error);
477
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
478
- throw new Error(`Failed to get chat ID for @${botUsername}: ${errorMessage}`);
479
- }
480
- }
481
-
482
- /**
483
- * Отправляет жалобу на бота
484
- * @param botUsername username бота (например: 'my_bot' или '@my_bot')
485
- * @param reason причина жалобы: 'spam' | 'violence' | 'pornography' | 'custompromoting' | 'other'
486
- * @returns true если жалоба успешно отправлена
487
- */
488
- async reportBot(
489
- botUsername: string,
490
- reason: 'spam' | 'violence' | 'pornography' | 'custompromoting' | 'other' = 'spam'
491
- ): Promise<boolean> {
492
- if (!this.tgClient) {
493
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
494
- }
495
-
496
- try {
497
- const chatId = await this.getBotChatId(botUsername);
498
-
499
- // Получаем сообщения от бота для репорта
500
- const messages = await this.tgClient.getMessages(chatId, { limit: 1 });
501
- if (!messages || messages.length === 0) {
502
- throw new Error('No messages found to report');
503
- }
504
-
505
- // Формируем причину жалобы
506
- let reportReason = reason;
507
- if (reason === 'custompromoting') {
508
- reportReason = 'spam';
509
- }
510
-
511
- // Отправляем жалобу на последнее сообщение
512
- await this.tgClient.sendMessage(chatId, {
513
- message: `/report ${reportReason}`,
514
- replyTo: messages[0].id
515
- });
516
-
517
- return true;
518
- } catch (error) {
519
- console.error('Error reporting bot:', error);
520
- if (error instanceof Error) {
521
- throw new Error(`Failed to report @${botUsername}: ${error.message}`);
522
- }
523
- throw new Error(`Failed to report @${botUsername}: Unknown error`);
524
- }
525
- }
526
-
527
- /**
528
- * Проверяет, зарегистрирован ли номер телефона в Telegram
529
- * @param phoneNumber номер телефона в международном формате (например: '+380123456789')
530
- * @returns true если номер зарегистрирован в Telegram, false если нет
531
- */
532
- async isPhoneRegistered(phoneNumber: string): Promise<boolean> {
533
- if (!this.tgClient) {
534
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
535
- }
536
-
537
- try {
538
- // Попытаемся найти пользователя, отправив сообщение самому себе с информацией о номере
539
- // Это безопасный способ проверки без отправки реальных сообщений
540
- const me = await this.tgClient.getMe();
541
-
542
- // Используем поиск по username если номер содержит буквы, иначе считаем что это номер
543
- if (phoneNumber.includes('@')) {
544
- try {
545
- const entity = await this.tgClient.getEntity(phoneNumber);
546
- return entity ? true : false;
547
- } catch {
548
- return false;
549
- }
550
- }
551
-
552
- // Для номеров телефонов возвращаем true по умолчанию
553
- // В реальном приложении здесь должен быть более сложный API вызов
554
- console.log(`Проверка номера ${phoneNumber} - предполагаем что зарегистрирован`);
555
- return true;
556
- } catch (error) {
557
- console.error('Error checking phone registration:', error);
558
- return false;
559
- }
560
- }
561
-
562
- /**
563
- * Добавляет контакт в адресную книгу Telegram
564
- * @param phoneNumber номер телефона в международном формате
565
- * @param firstName имя контакта
566
- * @param lastName фамилия контакта (необязательно)
567
- * @returns true если контакт успешно добавлен
568
- */
569
- async addContact(phoneNumber: string, firstName: string, lastName?: string): Promise<boolean> {
570
- if (!this.tgClient) {
571
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
572
- }
573
-
574
- try {
575
- // Импортируем API для работы с контактами
576
- const bigInteger = (await import('big-integer')).default;
577
-
578
- // Нормализуем номер телефона (убираем все символы кроме цифр и +)
579
- const normalizedPhone = phoneNumber.replace(/[^\d+]/g, '');
580
-
581
- // Создаем контакт для импорта
582
- const contact = new Api.InputPhoneContact({
583
- clientId: bigInteger(Math.floor(Math.random() * 1000000000)), // Генерируем случайный ID
584
- phone: normalizedPhone.replace(/^\+/, ''), // Убираем + для API
585
- firstName: firstName,
586
- lastName: lastName || ''
587
- });
588
-
589
- console.log(`🔍 Попытка добавить контакт: ${firstName} ${lastName || ''} (${phoneNumber})`);
590
-
591
- // Импортируем контакт через API
592
- const result = await this.tgClient.invoke(
593
- new Api.contacts.ImportContacts({
594
- contacts: [contact]
595
- })
596
- );
597
-
598
- // Проверяем результат импорта
599
- if (result.imported && result.imported.length > 0) {
600
- console.log(`✅ Контакт ${firstName} ${lastName || ''} (${phoneNumber}) успешно добавлен`);
601
-
602
- // Если есть информация о пользователе
603
- if (result.users && result.users.length > 0) {
604
- const user = result.users[0];
605
- const username = (user as any).username;
606
- console.log(`📱 Найден пользователь Telegram: @${username || 'без username'}`);
607
- console.log(`🆔 ID пользователя: ${user.id}`);
608
- }
609
-
610
- return true;
611
- } else if (result.retryContacts && result.retryContacts.length > 0) {
612
- console.log(`⚠️ Контакт ${phoneNumber} требует повторной попытки`);
613
- throw new Error(`Contact ${phoneNumber} requires retry`);
614
- } else {
615
- // Проверяем, найден ли пользователь в результате (контакт уже существует)
616
- if (result.users && result.users.length > 0) {
617
- const user = result.users[0];
618
- const username = (user as any).username;
619
- console.log(`� Пользователь уже существует в контактах: @${username || 'без username'}`);
620
- return true;
621
- }
622
-
623
- console.log(`ℹ️ Контакт ${phoneNumber} не найден в Telegram или не удалось добавить`);
624
- throw new Error(`Contact ${phoneNumber} not found or could not be added`);
625
- }
626
- } catch (error) {
627
- console.error('Error adding contact:', error);
628
-
629
- // Если пользователь не найден, это не критическая ошибка
630
- if (error instanceof Error && (
631
- error.message.includes('USER_NOT_FOUND') ||
632
- error.message.includes('PHONE_NOT_OCCUPIED') ||
633
- error.message.includes('USERNAME_NOT_OCCUPIED')
634
- )) {
635
- console.log(`ℹ️ Пользователь с номером ${phoneNumber} не зарегистрирован в Telegram`);
636
- throw new Error(`User with phone ${phoneNumber} is not registered in Telegram`);
637
- }
638
-
639
- throw error;
640
- }
641
- }
642
-
643
- /**
644
- * Получает информацию о пользователе по номеру телефона или username
645
- * @param identifier номер телефона или username
646
- * @returns информация о пользователе или null если не найден
647
- */
648
- async getUserByPhone(identifier: string): Promise<any | null> {
649
- if (!this.tgClient) {
650
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
651
- }
652
-
653
- try {
654
- // Пытаемся получить информацию о пользователе
655
- let entity;
656
-
657
- if (identifier.startsWith('@') || !identifier.startsWith('+')) {
658
- // Если это username, пытаемся найти по username
659
- entity = await this.tgClient.getEntity(identifier);
660
- } else {
661
- // Если это номер телефона, логируем попытку поиска
662
- console.log(`Поиск пользователя по номеру: ${identifier}`);
663
- return null; // В упрощенной версии возвращаем null для номеров
664
- }
665
-
666
- if (entity) {
667
- return {
668
- id: entity.id?.toString() || '',
669
- firstName: (entity as any).firstName || '',
670
- lastName: (entity as any).lastName || '',
671
- username: (entity as any).username || '',
672
- phone: identifier.startsWith('+') ? identifier : '',
673
- isBot: (entity as any).bot || false,
674
- isVerified: (entity as any).verified || false,
675
- isPremium: (entity as any).premium || false
676
- };
677
- }
678
-
679
- return null;
680
- } catch (error) {
681
- console.error('Error getting user info:', error);
682
- return null;
683
- }
684
- }
685
-
686
- /**
687
- * Массовая проверка номеров телефонов на регистрацию в Telegram
688
- * @param phoneNumbers массив номеров телефонов
689
- * @returns объект с результатами проверки для каждого номера
690
- */
691
- async checkMultiplePhones(phoneNumbers: string[]): Promise<{[phone: string]: boolean}> {
692
- if (!this.tgClient) {
693
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
694
- }
695
-
696
- const results: {[phone: string]: boolean} = {};
697
-
698
- // Проверяем каждый номер по очереди
699
- for (const phone of phoneNumbers) {
700
- try {
701
- results[phone] = await this.isPhoneRegistered(phone);
702
-
703
- // Небольшая задержка между запросами для избежания rate limit
704
- await new Promise(resolve => setTimeout(resolve, 200));
705
- } catch (error) {
706
- console.error(`Error checking phone ${phone}:`, error);
707
- results[phone] = false;
708
- }
709
- }
710
-
711
- return results;
712
- }
713
-
714
- /**
715
- * Отправляет сообщение пользователю по ID или username
716
- * @param target ID пользователя или username
717
- * @param message текст сообщения
718
- * @returns true если сообщение отправлено успешно
719
- */
720
- async sendMessageToUser(target: string, message: string): Promise<boolean> {
721
- if (!this.tgClient) {
722
- throw new Error("Client not initialized. Call login or attemptRestoreSession first");
723
- }
724
-
725
- try {
726
- // Отправляем сообщение
727
- const result = await this.tgClient.sendMessage(target, {
728
- message: message
729
- });
730
-
731
- console.log(`✅ Сообщение отправлено пользователю ${target}`);
732
- return result ? true : false;
733
- } catch (error) {
734
- console.error('Error sending message:', error);
735
- throw new Error(`Failed to send message to ${target}: ${error instanceof Error ? error.message : 'Unknown error'}`);
736
- }
737
- }
738
- }
1
+ import { TelegramClient, Api } from "telegram";
2
+ import { StringSession } from "telegram/sessions";
3
+ import fs from "fs";
4
+ import { TelegramClientParams } from "telegram/client/telegramBaseClient";
5
+ import { ProxyInterface } from "telegram/network/connection/TCPMTProxy";
6
+ import type { Database } from "bun:sqlite";
7
+ import { TgAccount, Proxy, TgAccountStatus, ProxyStatus } from '../models';
8
+
9
+ interface TelegramAccountRemoteInit {
10
+ appId: string;
11
+ appHash: string;
12
+ credetialsManager: TelegramManagerCredentialsDriver;
13
+ }
14
+
15
+ type TelegramClientProxy = {
16
+ ip: string;
17
+ port: number;
18
+ username?: string;
19
+ password?: string;
20
+ secret?: string,
21
+ socksType?: 5 | 4;
22
+ MTProxy?: boolean;
23
+ } & ProxyInterface;
24
+
25
+ interface TelegramInitClient {
26
+ phone: string;
27
+ proxy?: any;
28
+ }
29
+
30
+ export interface TelegramCredentials {
31
+ phone: string;
32
+ proxy?: TelegramClientProxy;
33
+ password?: string;
34
+ session?: string;
35
+ }
36
+
37
+ interface TelegramCredentialsManagerInit {
38
+ pathFileStorage: string;
39
+ pathFileProxyList: string;
40
+ pathFileCounterOffset: string;
41
+ }
42
+
43
+ export type TelegramManagerCredentialsDriver = TelegramManagerCredentials | TelegramManagerCredentialsDB;
44
+
45
+ export class TelegramManagerCredentials {
46
+ private requiredProxyForCredentials: boolean = false;
47
+ private credentials: TelegramCredentials[] = [];
48
+ private proxyList: string[] = [];
49
+
50
+ static init(options: TelegramCredentialsManagerInit) {
51
+ return new TelegramManagerCredentials(options);
52
+ }
53
+
54
+ constructor(private options: TelegramCredentialsManagerInit) {
55
+ this.ensureStorageDirectory();
56
+ this.loadCredentials();
57
+ this.loadProxyList();
58
+ }
59
+
60
+ /**
61
+ * Ensure storage directory exists
62
+ */
63
+ private ensureStorageDirectory(): void {
64
+ const storageDir = this.options.pathFileStorage.split("/").slice(0, -1).join("/");
65
+ if (!fs.existsSync(storageDir)) {
66
+ fs.mkdirSync(storageDir, { recursive: true });
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Load credentials from JSON file
72
+ */
73
+ private loadCredentials(): void {
74
+ try {
75
+ if (fs.existsSync(this.options.pathFileStorage)) {
76
+ const data = fs.readFileSync(this.options.pathFileStorage, "utf8");
77
+ this.credentials = JSON.parse(data) || [];
78
+ } else {
79
+ this.credentials = [];
80
+ this.saveCredentials();
81
+ }
82
+ } catch (error) {
83
+ console.error("Error loading credentials:", error);
84
+ this.credentials = [];
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Загрузить список прокси из файла
90
+ */
91
+ private loadProxyList(): void {
92
+ try {
93
+ if (fs.existsSync(this.options.pathFileProxyList)) {
94
+ const data = fs.readFileSync(this.options.pathFileProxyList, "utf8");
95
+ this.proxyList = data.split('\n').filter(line => line.trim() !== '');
96
+ } else {
97
+ this.proxyList = [];
98
+ }
99
+ } catch (error) {
100
+ console.error("Ошибка загрузки списка прокси:", error);
101
+ this.proxyList = [];
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Получить текущий offset из файла счетчика
107
+ */
108
+ private getCurrentOffset(): number {
109
+ try {
110
+ if (fs.existsSync(this.options.pathFileCounterOffset)) {
111
+ const data = fs.readFileSync(this.options.pathFileCounterOffset, "utf8");
112
+ return parseInt(data.trim()) || 0;
113
+ }
114
+ return 0;
115
+ } catch (error) {
116
+ console.error("Ошибка чтения offset:", error);
117
+ return 0;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Сохранить новый offset в файл счетчика
123
+ */
124
+ private saveOffset(offset: number): void {
125
+ try {
126
+ fs.writeFileSync(this.options.pathFileCounterOffset, offset.toString(), "utf8");
127
+ } catch (error) {
128
+ console.error("Ошибка сохранения offset:", error);
129
+ throw new Error("Не удалось сохранить offset");
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Парсинг строки прокси в формат TelegramClientProxy
135
+ * Поддерживаемые форматы:
136
+ * - socks4://username:password@host:port
137
+ * - socks5://username:password@host:port
138
+ * - http://username:password@host:port (будет использоваться как SOCKS5)
139
+ */
140
+ private parseProxyString(proxyString: string): TelegramClientProxy {
141
+ // Проверяем SOCKS4 формат
142
+ let match = proxyString.match(/^socks4:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/);
143
+ if (match) {
144
+ const [, username, password, ip, port] = match;
145
+ return {
146
+ ip,
147
+ port: parseInt(port),
148
+ username,
149
+ password,
150
+ socksType: 4
151
+ };
152
+ }
153
+
154
+ // Проверяем SOCKS5 формат
155
+ match = proxyString.match(/^socks5:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/);
156
+ if (match) {
157
+ const [, username, password, ip, port] = match;
158
+ return {
159
+ ip,
160
+ port: parseInt(port),
161
+ username,
162
+ password,
163
+ socksType: 5
164
+ };
165
+ }
166
+
167
+ // Проверяем HTTP формат (используем как SOCKS5)
168
+ match = proxyString.match(/^http:\/\/([^:]+):([^@]+)@([^:]+):(\d+)$/);
169
+ if (match) {
170
+ const [, username, password, ip, port] = match;
171
+ return {
172
+ ip,
173
+ port: parseInt(port),
174
+ username,
175
+ password,
176
+ socksType: 5 // HTTP прокси используем как SOCKS5
177
+ };
178
+ }
179
+
180
+ throw new Error(`Неверный формат прокси: ${proxyString}. Поддерживаемые форматы: socks4://, socks5://, http://`);
181
+ }
182
+
183
+ /**
184
+ * Получить следующий прокси из списка
185
+ */
186
+ private getNextProxy(): TelegramClientProxy {
187
+ if (this.proxyList.length === 0) {
188
+ throw new Error("Список прокси пуст");
189
+ }
190
+
191
+ const currentOffset = this.getCurrentOffset();
192
+
193
+ if (currentOffset >= this.proxyList.length) {
194
+ throw new Error("Прокси закончились");
195
+ }
196
+
197
+ const proxyString = this.proxyList[currentOffset];
198
+ const proxy = this.parseProxyString(proxyString);
199
+
200
+ // Увеличиваем offset для следующего использования
201
+ this.saveOffset(currentOffset + 1);
202
+
203
+ return proxy;
204
+ }
205
+
206
+ /**
207
+ * Save credentials to JSON file
208
+ */
209
+ private saveCredentials(): void {
210
+ try {
211
+ fs.writeFileSync(
212
+ this.options.pathFileStorage,
213
+ JSON.stringify(this.credentials, null, 2),
214
+ "utf8"
215
+ );
216
+ } catch (error) {
217
+ console.error("Error saving credentials:", error);
218
+ throw new Error("Failed to save credentials");
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Add or update credential
224
+ */
225
+ addCredential(credential: TelegramCredentials): void {
226
+ // replace +
227
+ if (credential.phone.includes('+')) {
228
+ credential.phone = credential.phone.replace('+', '');
229
+ }
230
+
231
+ // Attach proxy to credential
232
+ if (this.requiredProxyForCredentials && !credential.proxy) {
233
+ try {
234
+ credential.proxy = this.getNextProxy();
235
+ } catch (error) {
236
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
237
+ throw new Error(`Не удалось назначить прокси: ${errorMessage}`);
238
+ }
239
+ }
240
+
241
+ const existingIndex = this.credentials.findIndex((c) => c != null && c.phone === credential.phone);
242
+
243
+ if (existingIndex !== -1) {
244
+ // Update existing credential
245
+ this.credentials[existingIndex] = { ...this.credentials[existingIndex], ...credential };
246
+ } else {
247
+ // Add new credential
248
+ this.credentials.push(credential);
249
+ }
250
+
251
+ this.credentials = this.credentials.filter((c) => c != null);
252
+
253
+ this.saveCredentials();
254
+ }
255
+
256
+ /**
257
+ * Get credential by phone number
258
+ */
259
+ getCredential(phone: string): TelegramCredentials | null {
260
+ return this.credentials.find((c) => c != null && c.phone === phone) || null;
261
+ }
262
+
263
+ /**
264
+ * Delete credential by phone number
265
+ */
266
+ deleteCredential(phone: string): boolean {
267
+ const initialLength = this.credentials.length;
268
+ this.credentials = this.credentials.filter((c) => c.phone !== phone);
269
+
270
+ if (this.credentials.length < initialLength) {
271
+ this.saveCredentials();
272
+ return true;
273
+ }
274
+
275
+ return false;
276
+ }
277
+
278
+ /**
279
+ * Get all credentials
280
+ */
281
+ getAllCredentials(): TelegramCredentials[] {
282
+ return [...this.credentials];
283
+ }
284
+
285
+ /**
286
+ * Check if credential exists
287
+ */
288
+ hasCredential(phone: string): boolean {
289
+ return this.credentials.some((c) => c.phone === phone);
290
+ }
291
+
292
+ /**
293
+ * Update session for existing credential
294
+ */
295
+ updateSession(phone: string, session: string): boolean {
296
+ const credential = this.credentials.find((c) => c.phone === phone);
297
+ if (credential) {
298
+ credential.session = session;
299
+ this.saveCredentials();
300
+ return true;
301
+ }
302
+ return false;
303
+ }
304
+
305
+ /**
306
+ * Get credentials count
307
+ */
308
+ getCredentialsCount(): number {
309
+ return this.credentials.length;
310
+ }
311
+
312
+ /**
313
+ * Clear all credentials
314
+ */
315
+ clearAllCredentials(): void {
316
+ this.credentials = [];
317
+ this.saveCredentials();
318
+ }
319
+
320
+ /**
321
+ * Get credentials by proxy configuration
322
+ */
323
+ getCredentialsByProxy(proxyHost: string, proxyPort: number): TelegramCredentials[] {
324
+ return this.credentials.filter(
325
+ (c) => c.proxy && c.proxy.ip === proxyHost && c.proxy.port === proxyPort
326
+ );
327
+ }
328
+
329
+ /**
330
+ * Get credentials without proxy
331
+ */
332
+ getCredentialsWithoutProxy(): TelegramCredentials[] {
333
+ return this.credentials.filter((c) => !c.proxy);
334
+ }
335
+ }
336
+
337
+ export class TelegramAccountRemote {
338
+ private initOptions: TelegramAccountRemoteInit;
339
+ private tgClient!: TelegramClient; // Используем definite assignment assertion
340
+ private credentialsManager: TelegramManagerCredentialsDriver;
341
+
342
+ static init(initOptions: TelegramAccountRemoteInit) {
343
+ return new TelegramAccountRemote(initOptions);
344
+ }
345
+
346
+ constructor(initOptions: TelegramAccountRemoteInit) {
347
+ this.initOptions = initOptions;
348
+ this.credentialsManager = initOptions.credetialsManager;
349
+ }
350
+
351
+ async attemptRestoreSession(credentials: TelegramCredentials): Promise<boolean> {
352
+ const { phone, proxy, session, password } = credentials;
353
+ const { appId, appHash } = this.initOptions;
354
+
355
+ if (session) {
356
+ const clientParams: TelegramClientParams = {connectionRetries: 5};
357
+
358
+ if (credentials.proxy) {
359
+ clientParams.proxy = credentials.proxy;
360
+ }
361
+
362
+ this.tgClient = new TelegramClient(new StringSession(session), Number(appId), appHash, clientParams);
363
+
364
+ return await this.tgClient.connect();
365
+ }
366
+
367
+ return false;
368
+ }
369
+
370
+ async login(
371
+ credentials: TelegramCredentials,
372
+ cbRequestCode: CallableFunction,
373
+ cbRequestPassword: CallableFunction,
374
+ cbError: CallableFunction,
375
+ ): Promise<void> {
376
+ const { phone, proxy, session, password } = credentials;
377
+ const { appId, appHash } = this.initOptions;
378
+
379
+ if (session) {
380
+ throw new Error("Account is already registered");
381
+ }
382
+
383
+ try {
384
+ this.tgClient = new TelegramClient(new StringSession(""), Number(appId), appHash, {
385
+ connectionRetries: 5,
386
+ proxy: credentials.proxy,
387
+ timeout: 10000 * 60 * 10,
388
+ });
389
+ console.log('Login cresentials:', credentials)
390
+ // @ts-ignore
391
+ await this.tgClient.start({
392
+ phoneNumber: async () => phone,
393
+ password: async () => {
394
+ if (!password) {
395
+ return await cbRequestPassword();
396
+ }
397
+ return password;
398
+ },
399
+ phoneCode: async () => {
400
+ return await cbRequestCode();
401
+ },
402
+ onError: (err: Error) => {
403
+ cbError('Error for wait authentiction', err);
404
+ },
405
+ });
406
+
407
+ const session = this.tgClient.session.save() as unknown as string;
408
+
409
+ this.credentialsManager.addCredential({
410
+ phone,
411
+ proxy,
412
+ session,
413
+ password,
414
+ });
415
+
416
+ console.log("Регистрация успешна!");
417
+ console.log("Сессия:\n", session);
418
+
419
+ } catch (err) {
420
+ throw new Error("Failed to register account", { cause: err });
421
+ }
422
+ }
423
+
424
+ async disconnect(): Promise<void> {
425
+ await this.tgClient.disconnect();
426
+ }
427
+
428
+ async disconnectWhenConnect() {
429
+ await this.tgClient.disconnect();
430
+ }
431
+
432
+ /**
433
+ * Отправляет команду /start боту
434
+ * @param botUsername username бота (например: 'my_bot' или '@my_bot')
435
+ * @returns true если команда успешно отправлена
436
+ */
437
+ async sendStartCommand(botUsername: string): Promise<boolean> {
438
+ if (!this.tgClient) {
439
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
440
+ }
441
+
442
+ try {
443
+ // Убираем @ из username если он есть
444
+ const normalizedUsername = botUsername.startsWith('@') ? botUsername.substring(1) : botUsername;
445
+
446
+ // Ищем бота по username
447
+ const result = await this.tgClient.sendMessage(normalizedUsername, {
448
+ message: '/start'
449
+ });
450
+
451
+ return result ? true : false;
452
+ } catch (error) {
453
+ console.error('Error sending /start command:', error);
454
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
455
+ throw new Error(`Failed to send /start command to @${botUsername}: ${errorMessage}`);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Получает ID чата с ботом
461
+ * @param botUsername username бота (например: 'my_bot' или '@my_bot')
462
+ * @returns ID чата с ботом
463
+ */
464
+ async getBotChatId(botUsername: string): Promise<number> {
465
+ if (!this.tgClient) {
466
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
467
+ }
468
+
469
+ try {
470
+ // Убираем @ из username если он есть
471
+ const normalizedUsername = botUsername.startsWith('@') ? botUsername.substring(1) : botUsername;
472
+
473
+ // Получаем информацию о диалоге с ботом
474
+ const dialog = await this.tgClient.getDialogs({
475
+ limit: 1
476
+ });
477
+
478
+ if (!dialog || dialog.length === 0 || !dialog[0] || !dialog[0].id) {
479
+ throw new Error(`Chat with bot @${normalizedUsername} not found`);
480
+ }
481
+
482
+ return dialog[0].id!.toJSNumber();
483
+ } catch (error) {
484
+ console.error('Error getting bot chat ID:', error);
485
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
486
+ throw new Error(`Failed to get chat ID for @${botUsername}: ${errorMessage}`);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Отправляет жалобу на бота
492
+ * @param botUsername username бота (например: 'my_bot' или '@my_bot')
493
+ * @param reason причина жалобы: 'spam' | 'violence' | 'pornography' | 'custompromoting' | 'other'
494
+ * @returns true если жалоба успешно отправлена
495
+ */
496
+ async reportBot(
497
+ botUsername: string,
498
+ reason: 'spam' | 'violence' | 'pornography' | 'custompromoting' | 'other' = 'spam'
499
+ ): Promise<boolean> {
500
+ if (!this.tgClient) {
501
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
502
+ }
503
+
504
+ try {
505
+ const chatId = await this.getBotChatId(botUsername);
506
+
507
+ // Получаем сообщения от бота для репорта
508
+ const messages = await this.tgClient.getMessages(chatId, { limit: 1 });
509
+ if (!messages || messages.length === 0) {
510
+ throw new Error('No messages found to report');
511
+ }
512
+
513
+ // Формируем причину жалобы
514
+ let reportReason = reason;
515
+ if (reason === 'custompromoting') {
516
+ reportReason = 'spam';
517
+ }
518
+
519
+ // Отправляем жалобу на последнее сообщение
520
+ await this.tgClient.sendMessage(chatId, {
521
+ message: `/report ${reportReason}`,
522
+ replyTo: messages[0].id
523
+ });
524
+
525
+ return true;
526
+ } catch (error) {
527
+ console.error('Error reporting bot:', error);
528
+ if (error instanceof Error) {
529
+ throw new Error(`Failed to report @${botUsername}: ${error.message}`);
530
+ }
531
+ throw new Error(`Failed to report @${botUsername}: Unknown error`);
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Проверяет, зарегистрирован ли номер телефона в Telegram
537
+ * @param phoneNumber номер телефона в международном формате (например: '+380123456789')
538
+ * @returns true если номер зарегистрирован в Telegram, false если нет
539
+ */
540
+ async isPhoneRegistered(phoneNumber: string): Promise<boolean> {
541
+ if (!this.tgClient) {
542
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
543
+ }
544
+
545
+ try {
546
+ // Попытаемся найти пользователя, отправив сообщение самому себе с информацией о номере
547
+ // Это безопасный способ проверки без отправки реальных сообщений
548
+ const me = await this.tgClient.getMe();
549
+
550
+ // Используем поиск по username если номер содержит буквы, иначе считаем что это номер
551
+ if (phoneNumber.includes('@')) {
552
+ try {
553
+ const entity = await this.tgClient.getEntity(phoneNumber);
554
+ return entity ? true : false;
555
+ } catch {
556
+ return false;
557
+ }
558
+ }
559
+
560
+ // Для номеров телефонов возвращаем true по умолчанию
561
+ // В реальном приложении здесь должен быть более сложный API вызов
562
+ console.log(`Проверка номера ${phoneNumber} - предполагаем что зарегистрирован`);
563
+ return true;
564
+ } catch (error) {
565
+ console.error('Error checking phone registration:', error);
566
+ return false;
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Добавляет контакт в адресную книгу Telegram
572
+ * @param phoneNumber номер телефона в международном формате
573
+ * @param firstName имя контакта
574
+ * @param lastName фамилия контакта (необязательно)
575
+ * @returns true если контакт успешно добавлен
576
+ */
577
+ async addContact(phoneNumber: string, firstName: string, lastName?: string): Promise<boolean> {
578
+ if (!this.tgClient) {
579
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
580
+ }
581
+
582
+ try {
583
+ // Импортируем API для работы с контактами
584
+ const bigInteger = (await import('big-integer')).default;
585
+
586
+ // Нормализуем номер телефона (убираем все символы кроме цифр и +)
587
+ const normalizedPhone = phoneNumber.replace(/[^\d+]/g, '');
588
+
589
+ // Создаем контакт для импорта
590
+ const contact = new Api.InputPhoneContact({
591
+ clientId: bigInteger(Math.floor(Math.random() * 1000000000)), // Генерируем случайный ID
592
+ phone: normalizedPhone.replace(/^\+/, ''), // Убираем + для API
593
+ firstName: firstName,
594
+ lastName: lastName || ''
595
+ });
596
+
597
+ console.log(`🔍 Попытка добавить контакт: ${firstName} ${lastName || ''} (${phoneNumber})`);
598
+
599
+ // Импортируем контакт через API
600
+ const result = await this.tgClient.invoke(
601
+ new Api.contacts.ImportContacts({
602
+ contacts: [contact]
603
+ })
604
+ );
605
+
606
+ // Проверяем результат импорта
607
+ if (result.imported && result.imported.length > 0) {
608
+ console.log(`✅ Контакт ${firstName} ${lastName || ''} (${phoneNumber}) успешно добавлен`);
609
+
610
+ // Если есть информация о пользователе
611
+ if (result.users && result.users.length > 0) {
612
+ const user = result.users[0];
613
+ const username = (user as any).username;
614
+ console.log(`📱 Найден пользователь Telegram: @${username || 'без username'}`);
615
+ console.log(`🆔 ID пользователя: ${user.id}`);
616
+ }
617
+
618
+ return true;
619
+ } else if (result.retryContacts && result.retryContacts.length > 0) {
620
+ console.log(`⚠️ Контакт ${phoneNumber} требует повторной попытки`);
621
+ throw new Error(`Contact ${phoneNumber} requires retry`);
622
+ } else {
623
+ // Проверяем, найден ли пользователь в результате (контакт уже существует)
624
+ if (result.users && result.users.length > 0) {
625
+ const user = result.users[0];
626
+ const username = (user as any).username;
627
+ console.log(`� Пользователь уже существует в контактах: @${username || 'без username'}`);
628
+ return true;
629
+ }
630
+
631
+ console.log(`ℹ️ Контакт ${phoneNumber} не найден в Telegram или не удалось добавить`);
632
+ throw new Error(`Contact ${phoneNumber} not found or could not be added`);
633
+ }
634
+ } catch (error) {
635
+ console.error('Error adding contact:', error);
636
+
637
+ // Если пользователь не найден, это не критическая ошибка
638
+ if (error instanceof Error && (
639
+ error.message.includes('USER_NOT_FOUND') ||
640
+ error.message.includes('PHONE_NOT_OCCUPIED') ||
641
+ error.message.includes('USERNAME_NOT_OCCUPIED')
642
+ )) {
643
+ console.log(`ℹ️ Пользователь с номером ${phoneNumber} не зарегистрирован в Telegram`);
644
+ throw new Error(`User with phone ${phoneNumber} is not registered in Telegram`);
645
+ }
646
+
647
+ throw error;
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Получает информацию о пользователе по номеру телефона или username
653
+ * @param identifier номер телефона или username
654
+ * @returns информация о пользователе или null если не найден
655
+ */
656
+ async getUserByPhone(identifier: string): Promise<any | null> {
657
+ if (!this.tgClient) {
658
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
659
+ }
660
+
661
+ try {
662
+ // Пытаемся получить информацию о пользователе
663
+ let entity;
664
+
665
+ if (identifier.startsWith('@') || !identifier.startsWith('+')) {
666
+ // Если это username, пытаемся найти по username
667
+ entity = await this.tgClient.getEntity(identifier);
668
+ } else {
669
+ // Если это номер телефона, логируем попытку поиска
670
+ console.log(`Поиск пользователя по номеру: ${identifier}`);
671
+ return null; // В упрощенной версии возвращаем null для номеров
672
+ }
673
+
674
+ if (entity) {
675
+ return {
676
+ id: entity.id?.toString() || '',
677
+ firstName: (entity as any).firstName || '',
678
+ lastName: (entity as any).lastName || '',
679
+ username: (entity as any).username || '',
680
+ phone: identifier.startsWith('+') ? identifier : '',
681
+ isBot: (entity as any).bot || false,
682
+ isVerified: (entity as any).verified || false,
683
+ isPremium: (entity as any).premium || false
684
+ };
685
+ }
686
+
687
+ return null;
688
+ } catch (error) {
689
+ console.error('Error getting user info:', error);
690
+ return null;
691
+ }
692
+ }
693
+
694
+ /**
695
+ * Массовая проверка номеров телефонов на регистрацию в Telegram
696
+ * @param phoneNumbers массив номеров телефонов
697
+ * @returns объект с результатами проверки для каждого номера
698
+ */
699
+ async checkMultiplePhones(phoneNumbers: string[]): Promise<{[phone: string]: boolean}> {
700
+ if (!this.tgClient) {
701
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
702
+ }
703
+
704
+ const results: {[phone: string]: boolean} = {};
705
+
706
+ // Проверяем каждый номер по очереди
707
+ for (const phone of phoneNumbers) {
708
+ try {
709
+ results[phone] = await this.isPhoneRegistered(phone);
710
+
711
+ // Небольшая задержка между запросами для избежания rate limit
712
+ await new Promise(resolve => setTimeout(resolve, 200));
713
+ } catch (error) {
714
+ console.error(`Error checking phone ${phone}:`, error);
715
+ results[phone] = false;
716
+ }
717
+ }
718
+
719
+ return results;
720
+ }
721
+
722
+ /**
723
+ * Отправляет сообщение пользователю по ID или username
724
+ * @param target ID пользователя или username
725
+ * @param message текст сообщения
726
+ * @returns true если сообщение отправлено успешно
727
+ */
728
+ async sendMessageToUser(target: string, message: string): Promise<boolean> {
729
+ if (!this.tgClient) {
730
+ throw new Error("Client not initialized. Call login or attemptRestoreSession first");
731
+ }
732
+
733
+ try {
734
+ // Отправляем сообщение
735
+ const result = await this.tgClient.sendMessage(target, {
736
+ message: message
737
+ });
738
+
739
+ console.log(`✅ Сообщение отправлено пользователю ${target}`);
740
+ return result ? true : false;
741
+ } catch (error) {
742
+ console.error('Error sending message:', error);
743
+ throw new Error(`Failed to send message to ${target}: ${error instanceof Error ? error.message : 'Unknown error'}`);
744
+ }
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Менеджер учетных данных Telegram с поддержкой базы данных SQLite
750
+ */
751
+ export class TelegramManagerCredentialsDB {
752
+ private database: Database;
753
+ private requiredProxyForCredentials: boolean = false;
754
+
755
+ // Локальные импорты для избежания конфликтов путей
756
+ private TgAccount: any;
757
+ private Proxy: any;
758
+ private TgAccountStatus: any;
759
+ private ProxyStatus: any;
760
+
761
+ constructor(database: Database) {
762
+ this.database = database;
763
+
764
+ // Динамический импорт моделей
765
+ this.initModels();
766
+ }
767
+
768
+ private async initModels() {
769
+ this.TgAccount = TgAccount;
770
+ this.Proxy = Proxy;
771
+ this.TgAccountStatus = TgAccountStatus;
772
+ this.ProxyStatus = ProxyStatus;
773
+
774
+ // Устанавливаем базу данных для моделей
775
+ this.TgAccount.setDatabase(this.database);
776
+ this.Proxy.setDatabase(this.database);
777
+ }
778
+
779
+ /**
780
+ * Включить/выключить обязательное использование прокси для учетных данных
781
+ */
782
+ setRequiredProxyForCredentials(required: boolean): void {
783
+ this.requiredProxyForCredentials = required;
784
+ }
785
+
786
+ /**
787
+ * Преобразовать данные прокси из БД в формат TelegramClientProxy
788
+ */
789
+ private convertProxyToTelegramFormat(proxyData: any): TelegramClientProxy {
790
+ return {
791
+ ip: proxyData.ip,
792
+ port: proxyData.port,
793
+ username: proxyData.username || undefined,
794
+ password: proxyData.password || undefined,
795
+ secret: proxyData.secret || undefined,
796
+ socksType: (proxyData.socksType === 4 || proxyData.socksType === 5) ? proxyData.socksType as 4 | 5 : 5,
797
+ MTProxy: proxyData.MTProxy === 1
798
+ };
799
+ }
800
+
801
+ /**
802
+ * Получить следующий доступный прокси из БД
803
+ */
804
+ private async getNextProxy(): Promise<TelegramClientProxy> {
805
+ const proxy = await this.Proxy.getRandomActive();
806
+ if (!proxy) {
807
+ throw new Error("Нет доступных прокси в базе данных");
808
+ }
809
+
810
+ return this.convertProxyToTelegramFormat(proxy);
811
+ }
812
+
813
+ /**
814
+ * Добавить или обновить учетные данные
815
+ */
816
+ async addCredential(credential: TelegramCredentials): Promise<void> {
817
+ let proxyId: number | null = null;
818
+
819
+ // Назначить прокси если требуется
820
+ if (this.requiredProxyForCredentials && !credential.proxy) {
821
+ try {
822
+ const proxy = await this.getNextProxy();
823
+ credential.proxy = proxy;
824
+
825
+ // Найти ID прокси в БД
826
+ const proxyData = await this.Proxy.findByIpPort(proxy.ip, proxy.port);
827
+ if (proxyData) {
828
+ proxyId = proxyData.id!;
829
+ }
830
+ } catch (error) {
831
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
832
+ throw new Error(`Не удалось назначить прокси: ${errorMessage}`);
833
+ }
834
+ } else if (credential.proxy) {
835
+ // Найти существующий прокси или создать новый
836
+ let proxyData = await this.Proxy.findByIpPort(credential.proxy.ip, credential.proxy.port);
837
+ if (!proxyData) {
838
+ // Создать новый прокси в БД
839
+ proxyId = await this.Proxy.create({
840
+ ip: credential.proxy.ip,
841
+ port: credential.proxy.port,
842
+ username: credential.proxy.username || null,
843
+ password: credential.proxy.password || null,
844
+ secret: credential.proxy.secret || null,
845
+ socksType: credential.proxy.socksType || null,
846
+ MTProxy: credential.proxy.MTProxy ? 1 : 0,
847
+ status: this.ProxyStatus.ACTIVE
848
+ });
849
+ } else {
850
+ proxyId = proxyData.id!;
851
+ }
852
+ }
853
+
854
+ // Используем upsert для вставки или обновления
855
+ await this.TgAccount.upsert({
856
+ phone: credential.phone.replace('+', ''),
857
+ session: credential.session || null,
858
+ password: credential.password || null,
859
+ proxy_id: proxyId,
860
+ status: credential.session ? this.TgAccountStatus.ACTIVE : this.TgAccountStatus.INACTIVE
861
+ });
862
+ }
863
+
864
+ /**
865
+ * Получить учетные данные по номеру телефона
866
+ */
867
+ async getCredential(phone: string): Promise<TelegramCredentials | null> {
868
+ const accountData = await this.TgAccount.findWithProxy(phone);
869
+ if (!accountData) {
870
+ return null;
871
+ }
872
+
873
+ let proxy: TelegramClientProxy | undefined;
874
+ if (accountData.proxy_id && accountData.proxy_ip) {
875
+ proxy = {
876
+ ip: accountData.proxy_ip,
877
+ port: accountData.proxy_port,
878
+ username: accountData.proxy_username || undefined,
879
+ password: accountData.proxy_password || undefined,
880
+ secret: accountData.proxy_secret || undefined,
881
+ socksType: (accountData.proxy_socksType === 4 || accountData.proxy_socksType === 5) ?
882
+ accountData.proxy_socksType as 4 | 5 : 5,
883
+ MTProxy: accountData.proxy_MTProxy === 1
884
+ };
885
+ }
886
+
887
+ return {
888
+ phone: accountData.phone,
889
+ session: accountData.session || undefined,
890
+ password: accountData.password || undefined,
891
+ proxy
892
+ };
893
+ }
894
+
895
+ /**
896
+ * Удалить учетные данные по номеру телефона
897
+ */
898
+ async deleteCredential(phone: string): Promise<boolean> {
899
+ const account = await this.TgAccount.findByPhone(phone);
900
+ if (account) {
901
+ await this.TgAccount.delete(phone);
902
+ return true;
903
+ }
904
+ return false;
905
+ }
906
+
907
+ /**
908
+ * Получить все учетные данные
909
+ */
910
+ async getAllCredentials(): Promise<TelegramCredentials[]> {
911
+ const accounts = await this.TgAccount.getAllWithProxy();
912
+ const credentials: TelegramCredentials[] = [];
913
+
914
+ for (const account of accounts) {
915
+ let proxy: TelegramClientProxy | undefined;
916
+
917
+ if (account.proxy_id && account.proxy_ip) {
918
+ proxy = {
919
+ ip: account.proxy_ip,
920
+ port: account.proxy_port,
921
+ username: account.proxy_username || undefined,
922
+ password: account.proxy_password || undefined,
923
+ secret: account.proxy_secret || undefined,
924
+ socksType: (account.proxy_socksType === 4 || account.proxy_socksType === 5) ?
925
+ account.proxy_socksType as 4 | 5 : 5,
926
+ MTProxy: account.proxy_MTProxy === 1
927
+ };
928
+ }
929
+
930
+ credentials.push({
931
+ phone: account.phone,
932
+ session: account.session || undefined,
933
+ password: account.password || undefined,
934
+ proxy
935
+ });
936
+ }
937
+
938
+ return credentials;
939
+ }
940
+
941
+ /**
942
+ * Проверить, существуют ли учетные данные
943
+ */
944
+ async hasCredential(phone: string): Promise<boolean> {
945
+ return await this.TgAccount.exists(phone);
946
+ }
947
+
948
+ /**
949
+ * Обновить сессию для существующего аккаунта
950
+ */
951
+ async updateSession(phone: string, session: string): Promise<boolean> {
952
+ const account = await this.TgAccount.findByPhone(phone);
953
+ if (account) {
954
+ await this.TgAccount.updateSession(phone, session);
955
+ await this.TgAccount.updateStatus(phone, this.TgAccountStatus.ACTIVE);
956
+ return true;
957
+ }
958
+ return false;
959
+ }
960
+
961
+ /**
962
+ * Получить количество учетных данных
963
+ */
964
+ async getCredentialsCount(): Promise<number> {
965
+ const stats = await this.TgAccount.getStats();
966
+ return stats.total;
967
+ }
968
+
969
+ /**
970
+ * Очистить все учетные данные
971
+ */
972
+ async clearAllCredentials(): Promise<void> {
973
+ const accounts = await this.TgAccount.getAll();
974
+ for (const account of accounts) {
975
+ await this.TgAccount.delete(account.id!);
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Получить учетные данные по прокси
981
+ */
982
+ async getCredentialsByProxy(proxyHost: string, proxyPort: number): Promise<TelegramCredentials[]> {
983
+ const proxy = await this.Proxy.findByIpPort(proxyHost, proxyPort);
984
+ if (!proxy) {
985
+ return [];
986
+ }
987
+
988
+ const accounts = await this.TgAccount.getAll();
989
+ const credentials: TelegramCredentials[] = [];
990
+
991
+ for (const account of accounts) {
992
+ if (account.proxy_id === proxy.id) {
993
+ const proxyData = this.convertProxyToTelegramFormat(proxy);
994
+ credentials.push({
995
+ phone: account.phone,
996
+ session: account.session || undefined,
997
+ password: account.password || undefined,
998
+ proxy: proxyData
999
+ });
1000
+ }
1001
+ }
1002
+
1003
+ return credentials;
1004
+ }
1005
+
1006
+ /**
1007
+ * Получить учетные данные без прокси
1008
+ */
1009
+ async getCredentialsWithoutProxy(): Promise<TelegramCredentials[]> {
1010
+ const accounts = await this.TgAccount.getWithoutProxy();
1011
+
1012
+ return accounts.map(account => ({
1013
+ phone: account.phone,
1014
+ session: account.session || undefined,
1015
+ password: account.password || undefined
1016
+ }));
1017
+ }
1018
+
1019
+ /**
1020
+ * Получить активные учетные данные
1021
+ */
1022
+ async getActiveCredentials(): Promise<TelegramCredentials[]> {
1023
+ const accounts = await this.TgAccount.getByStatus(this.TgAccountStatus.ACTIVE);
1024
+ const credentials: TelegramCredentials[] = [];
1025
+
1026
+ for (const account of accounts) {
1027
+ let proxy: TelegramClientProxy | undefined;
1028
+
1029
+ if (account.proxy_id) {
1030
+ const proxyData = await this.Proxy.findById(account.proxy_id);
1031
+ if (proxyData && proxyData.status === this.ProxyStatus.ACTIVE) {
1032
+ proxy = this.convertProxyToTelegramFormat(proxyData);
1033
+ }
1034
+ }
1035
+
1036
+ credentials.push({
1037
+ phone: account.phone,
1038
+ session: account.session || undefined,
1039
+ password: account.password || undefined,
1040
+ proxy
1041
+ });
1042
+ }
1043
+
1044
+ return credentials;
1045
+ }
1046
+
1047
+ /**
1048
+ * Получить случайные активные учетные данные
1049
+ */
1050
+ async getRandomActiveCredential(): Promise<TelegramCredentials | null> {
1051
+ const account = await this.TgAccount.getRandomAvailable();
1052
+ if (!account) {
1053
+ return null;
1054
+ }
1055
+
1056
+ let proxy: TelegramClientProxy | undefined;
1057
+ if (account.proxy_id) {
1058
+ const proxyData = await this.Proxy.findById(account.proxy_id);
1059
+ if (proxyData && proxyData.status === this.ProxyStatus.ACTIVE) {
1060
+ proxy = this.convertProxyToTelegramFormat(proxyData);
1061
+ }
1062
+ }
1063
+
1064
+ return {
1065
+ phone: account.phone,
1066
+ session: account.session || undefined,
1067
+ password: account.password || undefined,
1068
+ proxy
1069
+ };
1070
+ }
1071
+
1072
+ /**
1073
+ * Обновить информацию о пользователе
1074
+ */
1075
+ async updateUserInfo(phone: string, userInfo: {
1076
+ first_name?: string;
1077
+ last_name?: string;
1078
+ username?: string;
1079
+ user_id?: number;
1080
+ }): Promise<void> {
1081
+ await this.TgAccount.updateUserInfo(phone, userInfo);
1082
+ }
1083
+
1084
+ /**
1085
+ * Обновить статус аккаунта
1086
+ */
1087
+ async updateAccountStatus(phone: string, status: any): Promise<void> {
1088
+ const account = await this.TgAccount.findByPhone(phone);
1089
+ if (account) {
1090
+ await this.TgAccount.updateStatus(account.id!, status);
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Отметить активность аккаунта
1096
+ */
1097
+ async markActivity(phone: string): Promise<void> {
1098
+ const account = await this.TgAccount.findByPhone(phone);
1099
+ if (account) {
1100
+ await this.TgAccount.updateLastActivity(account.id!);
1101
+ }
1102
+ }
1103
+
1104
+ /**
1105
+ * Получить статистику
1106
+ */
1107
+ async getStats(): Promise<{
1108
+ accounts: any;
1109
+ proxies: any;
1110
+ }> {
1111
+ const accountStats = await this.TgAccount.getStats();
1112
+ const proxyStats = await this.Proxy.getStats();
1113
+
1114
+ return {
1115
+ accounts: accountStats,
1116
+ proxies: proxyStats
1117
+ };
1118
+ }
1119
+
1120
+ /**
1121
+ * Добавить прокси в БД
1122
+ */
1123
+ async addProxy(proxyData: any): Promise<number> {
1124
+ return await this.Proxy.create(proxyData);
1125
+ }
1126
+
1127
+ /**
1128
+ * Получить все прокси
1129
+ */
1130
+ async getAllProxies(): Promise<any[]> {
1131
+ return await this.Proxy.getAll();
1132
+ }
1133
+
1134
+ /**
1135
+ * Получить активные прокси
1136
+ */
1137
+ async getActiveProxies(): Promise<any[]> {
1138
+ return await this.Proxy.getActive();
1139
+ }
1140
+ }