commit fe21d04b1e9142e7126f50601ef1b635ef04e87b Author: krasi Date: Fri May 23 12:56:37 2025 +0800 one start diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5cd5bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,182 @@ +# Файлы и папки конфигурации Obsidian, создаваемые редактором заметок. +.obsidian +/.obsidian/ +.obsidian/* + +# Отдельные файлы (например, для хранения задач) +TODOS.md + +# Зависимости, генерируемые NPM и yarn, а также сборочные артефакты. +node_modules +dist + +# Конфигурации IDE и редакторов, чтобы не засорять репозиторий личными настройками. +.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +.fleet/ + +# Конфигурационные файлы для Visual Studio Code, кроме файлов настроек проекта. +.vs +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Системные файлы, генерируемые разными ОС. +.DS_Store +Thumbs.db + +# Лог-файлы, создаваемые NPM и yarn в случае ошибок. +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Виртуальные окружения Python. +.venv +/venv +.env +/env + +# Файлы SQLite базы данных. +db.sqlite3 + +# Файлы логов, используемые для отладки локального сервера. +*.log +*.log.* + +# Исключения файлов, заканчивающихся на "_00" +*_00 + +# ---> Python + +# Скомпилированные и оптимизированные файлы, которые не следует коммитить. +__pycache__/ +*.py[cod] +*$py.class + +# C-расширения. +*.so + +# Артефакты сборки и папки для управления пакетами Python. +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Файлы PyInstaller, содержащие шаблоны для создания exe. +*.manifest +*.spec + +# Логи установщика пакетов. +pip-log.txt +pip-delete-this-directory.txt + +# Отчеты о покрытии тестами и кэши тестирования. +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Файлы локализации. +*.mo +*.pot + +# Файлы, относящиеся к Django, Flask и другим фреймворкам. +local_settings.py +instance/ +.webassets-cache + +# Файлы, генерируемые при сборке документации Sphinx. +docs/_build/ + +# Директория PyBuilder. +.pybuilder/ +target/ + +# Папки для кэша и контрольных точек в Jupyter Notebook. +.ipynb_checkpoints + +# Файлы и папки IPython. +profile_default/ +ipython_config.py + +# Управление версиями Python (pyenv). +.python-version + +# Конфигурации pipenv, poetry, pdm и pytype. +.pdm.toml +__pypackages__/ +poetry.lock +pdm.lock + +# Файлы, генерируемые системами очередей и кэшей (например, Celery). +celerybeat-schedule +celerybeat.pid + +# Символы отладки Cython. +cython_debug/ + +# Кэш анализа типов. +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ + +# Папки и файлы конфигураций, создаваемые PyCharm и другими JetBrains IDE. +.idea/ +.history/* +.site +.spyderproject +.spyproject +.ropeproject + +# Папки и файлы конфигурации, связанные с медиаконтентом. +mediafiles/* +migrations/ +*.migrations/ +migrations/* +*/migrations/ +*/migrations/* +# Исключить все файлы в каталогах migrations, но оставить структуру директорий +migrations/**/* +*/**/migrations/**/* +!migrations/**/.keep +0001_* + + +# +.session +/session +*.session +*.csv + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..9079033 --- /dev/null +++ b/main.py @@ -0,0 +1,446 @@ +""" +# Конфигурационные данные +api_id = '1******1' # Замените на ваш API ID +api_hash = 'd******************************a' # Замените на ваш API Hash +phone_number = '+7999*******' # Замените на ваш номер телефона +channel_username_or_id = '-100**********' # Замените на username или ID группы +""" +import asyncio +import csv +import re +import os +import datetime +from telethon import TelegramClient, events +from telethon.tl.functions.channels import JoinChannelRequest +from telethon.tl.types import Channel, ChatForbidden, PeerChannel +from telethon.errors import ChannelPrivateError # Добавлен импорт +import logging +from tqdm import tqdm +import getpass # Для безопасного ввода пароля + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("telegram_parser.log", encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# Конфигурация +API_ID = '1******1' +API_HASH = 'd******************************a' +PHONE_NUMBER = '+7999*******' +# CHANNEL_IDENTIFIER теперь используется как значение по умолчанию при выборе диалога +CONFIG_CHANNEL_IDENTIFIER = '-**********' +CSV_FILENAME = 'telegram_data.csv' +# Дополнительные настройки +LIMIT_MESSAGES = None # None для всех сообщений, или число для ограничения +DAYS_LIMIT = None # None для всех сообщений, или число дней назад для ограничения + +async def parse_message(text: str) -> dict | None: + """ + Парсит текстовое сообщение с использованием регулярных выражений. + + Args: + text: Текст сообщения. + + Returns: + Словарь с извлеченными данными, или None, если не удалось найти необходимые поля. + """ + + raw_text_preview = text[:250] + ('...' if len(text) > 250 else '') # Немного увеличил превью + result = {'Raw Message': raw_text_preview} + + # Определяем тип сообщения + is_order = "Order #" in text and "Purchaser information:" in text + is_request_details = "Request details:" in text + + # --- Общая информация (обычно в конце) --- + additional_info_text = "" + # Ищем блок "Additional information:" + additional_info_match = re.search(r"Additional information:\s*\n(.*?)(?:-----|$)", text, re.DOTALL | re.IGNORECASE) + if additional_info_match: + additional_info_text = additional_info_match.group(1) + else: + # Если нет явного блока "Additional information:", но есть URL в конце, + # попробуем взять текст после последнего "-----\n" если он есть, или весь текст для поиска URL + parts = text.split("-----") + if len(parts) > 1: + potential_additional_info = parts[-1] + if "Transaction ID:" in potential_additional_info or "Block ID:" in potential_additional_info or "Form Name:" in potential_additional_info: + additional_info_text = potential_additional_info + + if additional_info_text: + transaction_id_match = re.search(r"Transaction ID:([^\n]+)", additional_info_text, re.IGNORECASE) + if transaction_id_match: result['Transaction ID'] = transaction_id_match.group(1).strip() + + block_id_match = re.search(r"Block ID:([^\n]+)", additional_info_text, re.IGNORECASE) + if block_id_match: result['Block ID'] = block_id_match.group(1).strip() + + form_name_match = re.search(r"Form Name:([^\n]+)", additional_info_text, re.IGNORECASE) + if form_name_match: result['Form Name'] = form_name_match.group(1).strip() + + # URL часто идет после Form Name или как отдельная строка в этом блоке + url_match_after_form_name = re.search(r"Form Name:[^\n]+\n\s*(`?(https?://\S+)`?)", additional_info_text, re.IGNORECASE) + if url_match_after_form_name: + result['URL'] = url_match_after_form_name.group(1).strip('`') + else: + # Ищем URL в любой строке блока Additional Information + url_fallback_match = re.search(r"^`?(https?://\S+)`?$", additional_info_text, re.MULTILINE | re.IGNORECASE) + if url_fallback_match: + result['URL'] = url_fallback_match.group(1).strip('`') + + if is_order: + result['Type'] = 'Order' + + order_id_match = re.search(r"Order #(\S+)", text, re.IGNORECASE) + if order_id_match: result['Order ID'] = order_id_match.group(1).strip() + + payment_amount_match = re.search(r"Payment Amount:([^\n]+)", text, re.IGNORECASE) + if payment_amount_match: result['Payment Amount'] = payment_amount_match.group(1).strip() + + # Информация о покупателе + purchaser_info_match = re.search(r"Purchaser information:\s*\n(.*?)(?:Additional information:|$)", text, re.DOTALL | re.IGNORECASE) + if purchaser_info_match: + purchaser_text = purchaser_info_match.group(1) + + name_match = re.search(r"(?:Name|Имя|ФИО|фио)[\s:]*([^\n]+)", purchaser_text, re.IGNORECASE) + if name_match: result['Name'] = name_match.group(1).strip() + + phone_match = re.search(r"(?:Phone|телефон|тел|номер)[\s:]*(\+?[0-9\s()\-]{10,})", purchaser_text, re.IGNORECASE) + if phone_match: result['Phone'] = phone_match.group(1).strip() + + email_match = re.search(r"(?:Email|почта|e-mail)[\s:]*([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)", purchaser_text, re.IGNORECASE) + if email_match: result['Email'] = email_match.group(1).strip() + + # Дата рождения или просто дата + date_match = re.search(r"(?:Дата_Рождения|Date)[\s:]*(\d{1,2}[-./]\d{1,2}[-./]\d{2,4})", purchaser_text, re.IGNORECASE) + if date_match: result['Date'] = date_match.group(1).strip() + + elif is_request_details: # Явно указано "Request details:" + result['Type'] = 'Request' + + # Извлекаем текст из секции "Request details:" + request_details_section_text = text + match_rd_section = re.search(r"Request details:\s*\n(.*?)(?:Additional information:|$)", text, re.DOTALL | re.IGNORECASE) + if match_rd_section: + request_details_section_text = match_rd_section.group(1) + + # Паттерны для "Request details" + request_patterns = { + 'Name': r'(?:Name|имя|ФИО|фио|namestep)[\s:]*([^\n]+)', + 'Phone': r'(?:Phone|телефон|тел|номер|phonenamber|phonestep)[\s:]*(\+?[0-9\s()\-]{10,})', + 'Email': r'(?:Email|почта|e-mail)[\s:]*([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)', + 'Date': r'(?:Date|дата)[\s:]*(\d{1,2}[-./]\d{1,2}[-./]\d{2,4})', + 'Checkbox': r'(?:Checkbox|чекбокс|галочка)[\s:]*([^\n]+)', + 'Selectbox': r'Selectbox[\s:]*([^\n]+)', + 'Request Type': r'^type[\s:]*([^\n]+)', # Добавил ^ для точности, если 'type' в начале строки + 'Persons': r'^Person[\s:]*([^\n]+)', + 'Helper': r'^helper[\s:]*([^\n]+)', + 'Connection': r'^connection[\s:]*([^\n]+)', + 'Comment': r'(?:comment|комментарий)[\s:]*([^\n]+)', + 'Address': r'(?:address|адрес)[\s:]*([^\n]+)', + 'Product': r'(?:product|товар|продукт)[\s:]*([^\n]+)', + 'Price': r'(?:price|цена|стоимость)[\s:]*([^\n]+)' + } + + for key, pattern_regex in request_patterns.items(): + match = re.search(pattern_regex, request_details_section_text, re.IGNORECASE | re.MULTILINE) + if match: + result[key] = match.group(1).strip() + else: + # Если нет явных признаков "Order" или "Request details", + # но есть поля, похожие на контактные данные в начале. + # Это может быть случай, когда "Request details:" отсутствует. + # Попробуем общие паттерны для имени и телефона. + # logger.debug(f"Сообщение без явных 'Order #' или 'Request details:'. Попытка общего парсинга: {raw_text_preview}") + + # Паттерны для "Request details" + fallback_patterns = { + 'Name': r'(?:Name|имя|ФИО|фио|namestep)[\s:]*([^\n]+)', + 'Phone': r'(?:Phone|телефон|тел|номер|phonenamber|phonestep)[\s:]*(\+?[0-9\s()\-]{10,})', + 'Email': r'(?:Email|почта|e-mail)[\s:]*([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)', + 'Date': r'(?:Date|дата)[\s:]*(\d{1,2}[-./]\d{1,2}[-./]\d{2,4})', + } + found_in_fallback = False + for key, pattern_regex in fallback_patterns.items(): + match = re.search(pattern_regex, text, re.IGNORECASE | re.MULTILINE) + if match: + # Проверяем, чтобы значение не было частью "Additional Information" если оно уже разобрано + if key not in result: # Чтобы не перезаписывать уже найденное из Additional Info + result[key] = match.group(1).strip() + found_in_fallback = True + if found_in_fallback: + result['Type'] = 'Request (Fallback)' + + + # Если URL не был найден в "Additional Information", попробуем найти его в основном тексте + if 'URL' not in result: + url_global_fallback_match = re.search(r"`?(https?://\S+)`?", text) + if url_global_fallback_match: + result['URL'] = url_global_fallback_match.group(1).strip('`') + + # Проверяем, есть ли хотя бы имя или телефон, или это заказ с ID + if result.get('Name') or result.get('Phone') or (is_order and result.get('Order ID')): + if 'Checkbox' in result and isinstance(result['Checkbox'], str): + result['Checkbox'] = result['Checkbox'].lower() + return result + + return None + +async def choose_dialog(client, configured_identifier): + """ + Позволяет пользователю выбрать целевой чат/канал. + """ + logger.info("Выберите целевой чат/канал:") + print("\nКак выбрать целевой чат/канал?") + print(f"1. Использовать ID из конфигурации: {configured_identifier}") + print("2. Ввести ID, username (@имя) или полную ссылку на чат/канал вручную.") + print("3. Выбрать из списка недавних диалогов (до 20).") + + choice = input("Ваш выбор (1-3): ") + identifier_to_use = None + + if choice == '1': + identifier_to_use = configured_identifier + if not identifier_to_use: + logger.warning("ID в конфигурации не указан.") + # Можно предложить ввести вручную или выбрать из списка + choice = '2' # Переходим к ручному вводу + + if choice == '2': # Также используется, если выбор '1' не удался + identifier_to_use = input("Введите ID, username (@имя) или ссылку: ").strip() + elif choice == '3': + try: + dialogs = await client.get_dialogs(limit=20) + if not dialogs: + logger.warning("Не найдено недавних диалогов.") + return None + + print("\nПоследние 20 диалогов:") + print("-" * 70) + print(f"{'№':<3} | {'Тип':<15} | {'Название':<30} | ID") + print("-" * 70) + for i, dialog in enumerate(dialogs): + entity_type = "Канал" if dialog.is_channel else "Группа" if dialog.is_group else "Пользователь" + name = dialog.name if hasattr(dialog, 'name') and dialog.name else "N/A" + name_short = name[:27] + "..." if len(name) > 30 else name + print(f"{i+1:<3} | {entity_type:<15} | {name_short:<30} | {dialog.id}") + print("-" * 70) + + dialog_choice_str = input(f"Выберите номер диалога (1-{len(dialogs)}) или 0 для отмены: ") + dialog_choice_num = int(dialog_choice_str) + + if 1 <= dialog_choice_num <= len(dialogs): + identifier_to_use = dialogs[dialog_choice_num-1].id # Используем ID напрямую + elif dialog_choice_num == 0: + logger.info("Выбор отменен.") + return None + else: + logger.warning("Неверный номер диалога.") + return None + except ValueError: + logger.warning("Неверный ввод для номера диалога.") + return None + except Exception as e: + logger.error(f"Ошибка при получении списка диалогов: {e}") + return None + + if not identifier_to_use: # Если ни один из вариантов не дал идентификатор + logger.error("Идентификатор чата/канала не был указан или выбран.") + return None + + try: + logger.info(f"Попытка получить информацию о сущности: {identifier_to_use}") + # Telethon get_entity очень гибкий: принимает int (ID), str (username, link, phone) + # Если это строка, которая выглядит как число (особенно отрицательное для channel_id), преобразуем в int + if isinstance(identifier_to_use, str): + if identifier_to_use.startswith('-') and identifier_to_use[1:].isdigit(): + entity = await client.get_entity(int(identifier_to_use)) + elif identifier_to_use.isdigit(): # Для обычных user ID или chat ID + entity = await client.get_entity(int(identifier_to_use)) + else: # Для username или https://t.me/joinchat/... или https://t.me/channel_name + entity = await client.get_entity(identifier_to_use) + else: # Если это уже int (например, из dialog.id) + entity = await client.get_entity(identifier_to_use) + return entity + except ValueError as e: + logger.error(f"Не удалось найти сущность по '{identifier_to_use}'. Убедитесь, что ID/имя пользователя/ссылка верны и у вас есть доступ. Ошибка: {e}") + return None + except TypeError as e: # Например, если передан None в get_entity + logger.error(f"Некорректный тип идентификатора для get_entity: {identifier_to_use}. Ошибка: {e}") + return None + except Exception as e: + logger.error(f"Непредвиденная ошибка при получении сущности '{identifier_to_use}': {e}") + return None + +async def main(): + """ + Основная функция для работы с Telegram и парсинга данных. + """ + logger.info("Запуск парсера Telegram") + + os.makedirs('sessions', exist_ok=True) + client = TelegramClient('sessions/telegram_session', API_ID, API_HASH) + + try: + await client.connect() + logger.info("Подключение к Telegram API установлено") + + if not await client.is_user_authorized(): + logger.info("Требуется авторизация") + await client.send_code_request(PHONE_NUMBER) + code = input('Введите код из Telegram: ') + try: + await client.sign_in(PHONE_NUMBER, code) + logger.info("Авторизация с кодом успешна") + except Exception as e: # Изначально ловим общее исключение + if "SESSION_PASSWORD_NEEDED" in str(e) or "Two-steps verification is enabled" in str(e): # Более точная проверка + logger.info("Требуется двухфакторная аутентификация (пароль)") + password = getpass.getpass('Введите пароль двухфакторной аутентификации: ') + try: + await client.sign_in(password=password) + logger.info("Авторизация с паролем успешна") + except Exception as e_pw: + logger.error(f"Ошибка авторизации с паролем: {e_pw}") + return + else: + logger.error(f"Ошибка авторизации с кодом: {e}") + return + + entity = await choose_dialog(client, CONFIG_CHANNEL_IDENTIFIER) + + if not entity: + logger.error("Не удалось определить целевой чат/канал. Завершение работы.") + return + + try: + # Проверка типа сущности и членства + if isinstance(entity, Channel): + logger.info(f"Тип: Канал/Супергруппа") + logger.info(f"Название: {entity.title}") + logger.info(f"ID: {entity.id}") + # logger.info(f"Доступ: {'публичный' if entity.megagroup else 'приватный'}") # megagroup не всегда показатель публичности + + # Проверка членства в группе/канале + # Для каналов/супергрупп, `entity.left` может быть True, если вы не участник. + # Для приватных каналов, к которым вы не присоединились, get_entity может не вернуть полную информацию или выдать ошибку ранее. + # Попытка присоединиться, если необходимо и возможно + try: + # Попытка получить полную информацию, чтобы проверить 'left' атрибут + full_entity = await client.get_input_entity(entity) # или get_dialogs с entity + # Проверка на участие может быть сложной, так как 'left' не всегда есть или актуален + # Простой способ - попытаться прочитать сообщения. Если не получится - значит нет доступа. + logger.info("Проверка доступа к сообщениям...") + async for _ in client.iter_messages(entity, limit=1): + break + logger.info("Доступ к сообщениям есть.") + + except (ChatForbidden, ChannelPrivateError) as e: # Telethon specific errors + logger.warning(f"Нет доступа к {entity.title} или он приватный и вы не участник: {e}") + logger.info(f"Попытка присоединиться к {entity.title}...") + try: + await client(JoinChannelRequest(entity)) + logger.info(f"Успешно присоединились к {entity.title}.") + except Exception as e_join: + logger.error(f"Ошибка при попытке присоединиться: {e_join}") + return + except Exception as e_check: + logger.warning(f"Не удалось точно определить статус участия, но сущность получена. Ошибка: {e_check}") + + + elif isinstance(entity, ChatForbidden): # Это обычно означает, что get_entity не смогла получить доступ + logger.error(f"ОШИБКА: Нет доступа к сущности '{entity.title if hasattr(entity, 'title') else CONFIG_CHANNEL_IDENTIFIER}' (запрещено или не существует)") + return + else: # User, Chat (обычная группа) + logger.info(f"Тип: {type(entity).__name__}, Название: {getattr(entity, 'title', getattr(entity, 'username', entity.id))}, ID: {entity.id}") + + + date_filter = None + if DAYS_LIMIT: + date_filter = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=DAYS_LIMIT) + logger.info(f"Будут обработаны сообщения не старше {DAYS_LIMIT} дней (с {date_filter.strftime('%d.%m.%Y %H:%M:%S %Z')})") + + # Настройка для прогресс-бара + messages_iterator = client.iter_messages(entity, limit=LIMIT_MESSAGES, reverse=True) + # Если LIMIT_MESSAGES is None, tqdm total будет None, что нормально. + # Если LIMIT_MESSAGES задан, используем его. + progress_bar_total = LIMIT_MESSAGES + + if LIMIT_MESSAGES is None: + logger.info("Обработка всех сообщений. Общее количество для прогресс-бара будет неизвестно.") + else: + logger.info(f"Будет обработано до {LIMIT_MESSAGES} сообщений.") + + parsed_data = [] + total_messages_processed = 0 + skipped_messages_by_date = 0 + + with tqdm(total=progress_bar_total, desc="Обработка сообщений", unit="сообщ.") as progress_bar: + async for message in messages_iterator: + progress_bar.update(1) + + if date_filter and message.date < date_filter: + skipped_messages_by_date += 1 + continue + + if message.text: + parsed = await parse_message(message.text) + if parsed: + parsed['Message Date'] = message.date.strftime('%d.%m.%Y %H:%M:%S') + parsed_data.append(parsed) + total_messages_processed += 1 + + logger.info(f"\nСтатистика:") + logger.info(f"Всего обработано сообщений в итераторе: {total_messages_processed}") + if DAYS_LIMIT: + logger.info(f"Пропущено сообщений (старше {DAYS_LIMIT} дней): {skipped_messages_by_date}") + logger.info(f"Найдено подходящих записей для CSV: {len(parsed_data)}") + + if parsed_data: + all_fields = set() + for entry in parsed_data: + all_fields.update(entry.keys()) + + fieldnames = sorted(list(all_fields)) + # Перемещаем основные поля в начало + preferred_order = [ + 'Message Date', 'Type', 'Name', 'Phone', 'Email', 'Date', + 'Form Name', 'Order ID', 'Payment Amount', 'Product', 'Price', + 'Selectbox', 'Checkbox', 'Request Type', 'Persons', 'Helper', 'Connection', + 'Transaction ID', 'Block ID', 'Address', 'Comment', 'URL', 'Raw Message' + ] + final_fieldnames = [f for f in preferred_order if f in fieldnames] + \ + [f for f in fieldnames if f not in preferred_order] + + with open(CSV_FILENAME, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.DictWriter(f, fieldnames=final_fieldnames) + writer.writeheader() + writer.writerows(parsed_data) + logger.info(f"Файл сохранен: {os.path.abspath(CSV_FILENAME)}") + else: + logger.warning("Нет данных для сохранения. Проверьте сообщения в чате или настройки парсера.") + + except Exception as e: + logger.error(f"Произошла ошибка при обработке сообщений: {e}", exc_info=True) + except Exception as e: + logger.error(f"Произошла ошибка при подключении или начальной настройке: {e}", exc_info=True) + finally: + if client.is_connected(): + await client.disconnect() + logger.info("Работа парсера завершена") + + +if __name__ == '__main__': + # Для Windows может потребоваться это для asyncio с tqdm в некоторых случаях + if os.name == 'nt': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Работа программы прервана пользователем") + except Exception as e: + logger.error(f"Критическая ошибка в __main__: {e}", exc_info=True) diff --git a/req.pip b/req.pip new file mode 100644 index 0000000..f356c48 --- /dev/null +++ b/req.pip @@ -0,0 +1,6 @@ +colorama==0.4.6 +pyaes==1.6.1 +pyasn1==0.6.1 +rsa==4.9.1 +Telethon==1.40.0 +tqdm==4.67.1 \ No newline at end of file