From c582a925c67c13ac08750c387fa057612f60d1d5 Mon Sep 17 00:00:00 2001 From: jsdio Date: Sun, 31 Oct 2021 21:22:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 29 ++++++ data/config.py.dist | 2 + data/install.bat | 3 + data/main.py | 135 ++++++++++++++++++++++++++ data/my_functions.py | 220 ++++++++++++++++++++++++++++++++++++++++++ data/requirements.txt | 2 + Запуск.lnk | Bin 0 -> 965 bytes Установка.lnk | Bin 0 -> 1543 bytes 9 files changed, 392 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 data/config.py.dist create mode 100644 data/install.bat create mode 100644 data/main.py create mode 100644 data/my_functions.py create mode 100644 data/requirements.txt create mode 100644 Запуск.lnk create mode 100644 Установка.lnk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab63d21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/data/config.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a7c160 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# TG-Parser +Парсер участников и сообщений из ТГ-Чатов и чатов для комментариев в ТГ-Каналах +## Возможности +- Выгрузка участников групп/каналов(по чату для комментариев) в json, excel и txt +- Выгрузка истории групп/каналов(по чату для комментариев) в html и txt. + +------------ + +## Установка +### Для Windows: + +- Запустить ярлык с названием **Установка** в главном каталоге либо запустить **install.bat** из каталога **data**. +- На сайте https://my.telegram.org/auth получить **api_id** и **api_hash** +- Записать данные в файл **config.py.dist** и переименовать его в **config.py** +### Для Linux: + +- Выполнить **pip install -r "requirements.txt"** из каталога **data**. +- На сайте https://my.telegram.org/auth получить **api_id** и **api_hash** +- Записать данные в файл **config.py.dist** и переименовать его в **config.py** +------------ + +## Использование +1. Запустить ярлык с названием **Запуск** в главном каталоге либо запустить **main.py** из каталога **data**. + 1.1 При первом запуске скрипт запросит у вас номер телефона. Это необходимо, чтобы скрипт авторизировался под вашим аккаунтом и мог собирать данные о чатах/каналах. +2. Введите ссылку на чат/канал либо id чата/канала, в которых состоит пользователь, под чьим именем используется скрипт. Обратите внимание, что некоторые TG-клиенты показывают ID чатов/каналов, убирая значение **-100** от начала ID. Например: + - **-100123456789** - Правильно + - **123456789** - Неправильно +3. После получения списка участников в главном каталоге появятся директории **Чаты** и **Каналы** в которых, в зависимости от вашего выбора, появятся папки с вашими чатами/каналами с обработанными данными. +4. Скрипт предложит вам сохранить историю сообщений. При утвердительном выборе сообщения начнут записываться в те же папки. За формат вывода особая благодарность [@danila_ms](https://t.me/danila_ms) diff --git a/data/config.py.dist b/data/config.py.dist new file mode 100644 index 0000000..754dca7 --- /dev/null +++ b/data/config.py.dist @@ -0,0 +1,2 @@ +api_id = 213213124 +api_hash = "asdawd23wd3hruid23" \ No newline at end of file diff --git a/data/install.bat b/data/install.bat new file mode 100644 index 0000000..53804fe --- /dev/null +++ b/data/install.bat @@ -0,0 +1,3 @@ +@echo off +python -m pip install -r requirements.txt +pause \ No newline at end of file diff --git a/data/main.py b/data/main.py new file mode 100644 index 0000000..d8b75f0 --- /dev/null +++ b/data/main.py @@ -0,0 +1,135 @@ +import json +from xlwt import Workbook +import xlwt +import config +import os +import asyncio +from my_functions import * + + +api_id = config.api_id +api_hash = config.api_hash +session = 'session.session' +loop = asyncio.get_event_loop() + +try: + # Получаем чат пользователя, проверяем, что за ссылку он отправил и ожидаем правильной ссылки + while True: + link = input('Введите ссылку на чат: ') + # link = ('osint_flood') + res = check_link(link) + if not res: + print('Неверная ссылка. Попробуйте другую.') + elif res == 'url' or res == 'id': + if res == 'id': + res = loop.run_until_complete(check_chat(link, 'id')) + else: + res = loop.run_until_complete(check_chat(link, 'url')) + if res is not False: + members = res[0] + admins = res[1] + chat = res[2] + users = res[3] + channel_type = res[4] + channel_title = res[5] + break + elif res == 'close': + chat = loop.run_until_complete(inv_chat(link)) + res = loop.run_until_complete(check_chat(chat, 'url')) + if res is not False: + members = res[0] + admins = res[1] + chat = res[2] + users = res[3] + channel_type = 'Чаты' + channel_title = chat.title + break + title = channel_title + for x in ['\\', '|', '"', '/', ':', + '?', '*', '<', '>']: + title = title.replace(x, ' ') + if os.path.exists(f'../Чаты') is False: + os.mkdir(f'../Чаты') + if os.path.exists(f'../Каналы') is False: + os.mkdir(f'../Каналы') + if os.path.exists(f'../{channel_type}/{title}') is False: + os.mkdir(f'../{channel_type}/{title}') + with open(f'../{channel_type}/{title}/Участники {title}.json', 'w', encoding='utf8') as f: + with open(f'../{channel_type}/{title}/Участники {title}.txt', 'w', encoding='utf8') as file: + all_users = { + 'admins': admins, + 'users': members + } + f.write(json.dumps(all_users, indent=4, ensure_ascii=False,)) + if admins is not None: + file.write('Администраторы:\n') + for x in admins: + file.write(f'{str(admins[x])}\n') + if len(members)>0: + file.write('Пользователи:\n') + for x in members: + file.write(f'{str(members[x])}\n') + wb = Workbook() + style = xlwt.easyxf('pattern: pattern solid, fore_colour light_blue;' + 'font: colour white, bold True;') + n_list = 1 + sheet1 = wb.add_sheet(f'Users_{n_list}') + sheet1.write(0, 0, 'Администраторы', style) + sheet1.write(0, 1, 'ID', style) + sheet1.write(0, 2, 'First Name', style) + sheet1.write(0, 3, 'Last Name', style) + sheet1.write(0, 4, 'Username', style) + sheet1.write(0, 5, 'Телефон', style) + sheet1.write(0, 6, 'Бот', style) + sheet1.write(0, 7, 'Удалён', style) + sheet1.write(0, 8, 'Скам', style) + n = 1 + q = 1 + for x in users: + sheet1.col(0).width = 256 * 17 + sheet1.col(1).width = 256 * 17 + sheet1.col(2).width = 256 * 25 + sheet1.col(3).width = 256 * 25 + sheet1.col(4).width = 256 * 25 + sheet1.col(5).width = 256 * 17 + sheet1.col(6).width = 256 * 7 + sheet1.col(7).width = 256 * 7 + sheet1.col(8).width = 256 * 7 + sheet1.write(n, 0, x['admin']) + sheet1.write(n, 1, x['id']) + sheet1.write(n, 2, x['first_name']) + sheet1.write(n, 3, x['last_name']) + sheet1.write(n, 4, x['username']) + sheet1.write(n, 5, x['phone']) + sheet1.write(n, 6, x['bot']) + sheet1.write(n, 7, x['deleted']) + sheet1.write(n, 8, x['scam']) + n += 1 + q += 1 + if n == 30000: + n_list += 1 + sheet1 = wb.add_sheet(f'Users_{n_list}"') + n = 1 + wb.save(f'../{channel_type}/{title}/Участники {title}.xls') + + + + while True: + otvet = input('''\nЖелаете ли вы сохранить историю сообщений? + 1 - да + 2 - нет ''') + if str(otvet) == '1' or str(otvet) == '2': + break + if str(otvet) == '1': + loop.run_until_complete(dump_messages(chat, title)) + input('\nСканирование закончено. Можете нажать "Enter", чтобы закрыть окно.') + +except Exception as e: + print(f'''Упс... Возникла ошибка +Текст ошибки: + +{e} + +Отправьте скриншот разработчику.''') + raise e + input('\nНажмите "Enter", чтобы закрыть окно.') diff --git a/data/my_functions.py b/data/my_functions.py new file mode 100644 index 0000000..f1fec8d --- /dev/null +++ b/data/my_functions.py @@ -0,0 +1,220 @@ +import re +import config +from telethon.sync import TelegramClient +from telethon.tl.types import ChannelParticipantsAdmins +from telethon import functions, errors +from progress.spinner import Spinner + +api_id = config.api_id +api_hash = config.api_hash +session = 'session.session' + + +async def inv_chat(link): + hash = link.rsplit('/', 1)[1] + async with TelegramClient(session, api_id, api_hash) as client: + try: + await client(functions.messages.ImportChatInviteRequest( + hash=hash)) + res = await client(functions.messages.CheckChatInviteRequest( + hash=hash + )) + if res.chat.megagroup is False: + print( + 'Похоже, вы отправили ссылку на закрытый канал. Уьедитесь, что вы собираете информацию из группы.') + exit() + except errors.ChannelsTooMuchError: + print('Вы вступили в слишком большое количество чатов') + exit() + except errors.InviteHashEmptyError: + print('Хеш приглашения пуст.') + exit() + except errors.InviteHashExpiredError: + print('Срок действия чата, к которому пользователь пытался присоединиться, истек, и он больше не действителен.') + exit() + except errors.InviteHashInvalidError: + print('Недействительная ссылка.') + exit() + except errors.SessionPasswordNeededError: + print('Включена двухэтапная проверка, требуется пароль.') + exit() + except errors.UsersTooMuchError: + print('Превышено максимальное количество пользователей (например, для создания чата).') + exit() + except errors.UserAlreadyParticipantError: + res = await client(functions.messages.CheckChatInviteRequest( + hash=hash + )) + return res.chat + + +async def check_chat(chat, type_link): + # Проверка на то, является ли ссылка на чат чатом с последующей выгрузкой участников + async with TelegramClient('session', api_id, api_hash) as client: + try: + if type_link == 'id': + ch = await client.get_entity(int(chat)) + else: + ch = await client.get_entity(chat) + channel_type = 'Чаты' + if ch.__class__.__name__ == 'Channel': + if ch.megagroup is False: + res = await client(functions.channels.GetFullChannelRequest( + channel=ch + )) + if len(res.chats) != 2: + print("Канал не имеет закреплённого чата для комментариев") + return False + else: + channel_type = 'Каналы' + channel_title = ch.title + ch = await client.get_entity(res.chats[1]) + count_members = await client(functions.channels.GetFullChannelRequest(channel=ch)) + count_members = count_members.full_chat.participants_count + if count_members > 10000: + print(f'Количество участников чата "{ch.title}" насчитывает более 10 тысяч человек. Выбран "обычный" + "агрессивный" режим.') + aggressive = True + else: + print(f'Количество участников чата "{ch.title}" насчитывает менее 10 тысяч человек. Выбран "обычный" режим.') + aggressive = False + admins = [] + titles = {} + async for user in client.iter_participants(ch, filter=ChannelParticipantsAdmins): + admins.append(user) + title = await client.get_permissions(ch, user) + titles[f'{title.participant.user_id}'] = title.participant.rank + if len(admins) == 0: + admins = None + else: + admins = list_users(admins, titles) + members = await client.get_participants(ch, aggressive=False) + if len(members) == 0: + members = None + else: + members = list_users(members) + if aggressive is True: + ag_members = await client.get_participants(ch, aggressive=True) + if len(ag_members) == 0: + ag_members = None + else: + ag_members = list_users(ag_members) + members = {**members, **ag_members} + if channel_type == 'Каналы': + limit = 3000 + print( + f'Собираем сообщения. В зависимости от ваших прошлый запросов, действие может занять продолжительное время.\n' + f'Лимит - {limit}') + mess = await client(functions.messages.GetHistoryRequest( + peer=ch, + offset_id=0, + offset_date=None, + add_offset=0, + limit=limit, + max_id=0, + min_id=0, + hash=0 + )) + mess_user = list_users(mess.users) + members = {**members, **mess_user} + users = [] + for x in members: + user = {} + if admins is not None: + if str(members[x]['id']) in admins: + user['admin'] = admins[str(members[x]['id'])]['title'] + else: + user['admin'] = '' + else: + user['admin'] = '' + user['id'] = members[x]['id'] + user['first_name'] = members[x]['first_name'] + if members[x]['last_name'] is None: + user['last_name'] = '' + else: + user['last_name'] = members[x]['last_name'] + if members[x]['username'] is None: + user['username'] = '' + else: + user['username'] = members[x]['username'] + if members[x]['phone'] is None: + user['phone'] = '' + else: + user['phone'] = members[x]['phone'] + if members[x]['bot'] is False: + user['bot'] = '' + else: + user['bot'] = 'True' + if members[x]['deleted'] is False: + user['deleted'] = '' + else: + user['deleted'] = 'True' + if members[x]['scam'] is False: + user['scam'] = '' + else: + user['scam'] = 'True' + users.append(user) + print(channel_type) + if channel_type != 'Каналы': + channel_title = ch.title + return members, admins, ch, users, channel_type, channel_title + else: + print('Вы ввели ссылку, которая не ведёт на открытую группу. Попробуйте другую.') + return False + except ValueError as e: + return False + + +async def dump_messages(chat, title): + """Выгружаем сообщения""" + async with TelegramClient(session, api_id, api_hash) as client: + with open(f'../Чаты/{title}/Сообщения {title}.txt', 'w', encoding='utf8') as file: + with open(f'../Чаты/{title}/Сообщения {title}.html', 'w', encoding='utf8') as f: + async for message in client.iter_messages(chat): + file.write(f'{message}\n') + if message.media is not None: + f.write( + f'
{message.from_id} | {message.date} Image.
{message.message}

Message id:{message.id}
\n') + else: + f.write( + f'
{message.from_id} | {message.date} {message.message}

{message}
\n') + + +def check_link(link): + try: + if int(link): + return 'id' + except Exception as e: + pass + """Проверяем ссылку регуляркой и определяем, что хочет пользователь""" + if re.match(r'https://t.me/joinchat/[a-z-_0-9]{1}[a-z-_0-9]{4,}$', link.lower()) or re.match( + r'http://t.me/joinchat/[a-z-_A-Z0-9]{1}[a-z-_0-9]{4,}$', link.lower()): + return 'close' + elif re.match(r'https://t.me/[a-z]{1}[a-z_0-9]{4,31}$', link.lower()) or re.match( + r'@[a-z]{1}[a-z_0-9]{4,31}$', link.lower()) or re.match( + r'[a-z]{1}[a-z_0-9]{4,31}$', link.lower() + ): + return 'url' + else: + return False + + +def list_users(*args): + members = args[0] + users = {} + for user in members: + users[f'{user.id}'] = { + 'id': user.id, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'username': user.username, + 'phone': user.phone, + 'bot': user.bot, + 'deleted': user.deleted, + 'scam': user.scam, + } + if len(args) == 2: + titles = args[1] + for key, value in titles.items(): + users[key]['title'] = value + return users + diff --git a/data/requirements.txt b/data/requirements.txt new file mode 100644 index 0000000..92a06ed --- /dev/null +++ b/data/requirements.txt @@ -0,0 +1,2 @@ +telethon==1.22.0 +xlwt==1.3.0 \ No newline at end of file diff --git a/Запуск.lnk b/Запуск.lnk new file mode 100644 index 0000000000000000000000000000000000000000..faf6a94aac21b19c862648cdfae7980bf95a5676 GIT binary patch literal 965 zcmeZaU|?VrVFHp23s zB@93#7^E0r=06J(G#6ky; zGZ+%#=BF{3g5B^e=u5N!g8+*yivf!xixZf%V6k9fU~qx@1Ed0kLH_8I<-}r7?*vIk zY$hl%_(4PISx|l~&=-j%i3|)@aC7rxLE<2Dmx9ejkCI0>3>Z?tZU$;&5Mc;oFk%2j z-_;=RDg}nz#LPUsf=UJlxOt^O^GdA}Yr zFJeF*j)A$8fe&bD4BSI8XiiaQ@Bju)4Aukz(uEm~F<7DvVSWO{;R_;wGzg@m7H5~_ z7wDRl7AK{YnKEqno4vNB_>t#CNp7F0seym?mgr{M-+L`M@wLPOUsaF8gyda;W&#t# z0!tum7#tA1Q7qSGZEu0cat{Z+CAJkkvOw_+AO?Dif#KMv$p`sNPU+~buA92s&P4f8RM^9k zNkLK+hNR|01qDeE6fPC^R8o-IL-bUUh4|K6{m-o9N3HZy7ykF&IdkssoO|z?IY2~) z_%QTDV_Hw8Hzhg3F?V(5#X!a>v8^jDX8$Oe9 z>=;Z^TcULxH&&CEx=1A#c?e4>f$HX&<>K+nVER06XS1ph6^Ecsg{1zB6hQTX=q4vM zgO25txIj)6qXZ+VtzOxuIDLy`YxURrvoM0q(OPbj~3|cGbGWZJUSPt@$`J5ZX zKFLV73Y%DCJB_0$ozHoLczR2a zB=bcXRwCPAuIvqGYHAN`bh%c+7&hIiVP66%@$##~LvbN-J17l788eem9b> zQ$~JDTbC^HzNMwqGkEUZs%tu^6ursbe1lhj6V; literal 0 HcmV?d00001