-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtelegram_member_scraper.py
More file actions
279 lines (238 loc) · 10.7 KB
/
telegram_member_scraper.py
File metadata and controls
279 lines (238 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
import asyncio
import os
import platform
import sys
import time
import aiosqlite
from telethon.sync import TelegramClient
from telethon.tl.functions.channels import InviteToChannelRequest
from telethon.errors import (FloodWaitError, UserPrivacyRestrictedError,
PeerFloodError, SessionPasswordNeededError,
UserIdInvalidError)
from config import ACCOUNTS
DATABASE_FILE = "db/added_members.db" # Path to the SQLite database
INITIAL_DELAY = 20 # Initial delay in seconds
DELAY_BETWEEN_ADDS = 20 # Delay between adds for a single client
DELAY_BETWEEN_CONCURRENT = 1 # Small delay between concurrent adds (in seconds)
def password_input(prompt='Password: '):
"""Cross-platform password input with asterisks."""
if platform.system() == 'Windows':
import msvcrt
print(prompt, end='', flush=True)
buf = []
while True:
ch = msvcrt.getwch()
if ch in ('\r', '\n'): # Enter
print('')
return ''.join(buf)
elif ch == '\b': # Backspace
if buf:
buf.pop()
sys.stdout.write('\b \b')
sys.stdout.flush()
elif ch == '\x03': # Ctrl+C
raise KeyboardInterrupt
else:
buf.append(ch)
sys.stdout.write('*')
sys.stdout.flush()
else:
import termios
import tty
print(prompt, end='', flush=True)
buf = []
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
while True:
ch = sys.stdin.read(1)
if ch in ('\r', '\n'): # Enter
print('')
return ''.join(buf)
elif ch == '\x7f': # Backspace
if buf:
buf.pop()
sys.stdout.write('\b \b')
sys.stdout.flush()
elif ch == '\x03': # Ctrl+C
raise KeyboardInterrupt
else:
buf.append(ch)
sys.stdout.write('*')
sys.stdout.flush()
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def parse_group_identifier(identifier):
"""Parses group identifier (username or chat ID)."""
if identifier.startswith('@') or identifier.startswith('https://t.me/'):
return identifier # Let get_entity handle usernames
try:
chat_id = int(identifier)
if chat_id >= 0:
raise ValueError("Chat ID must be negative.")
return chat_id
except ValueError:
raise ValueError("Invalid group identifier. Use @username or a negative integer chat ID.")
def parse_member_selection(selection_str, max_members):
"""Parses member selection string (individual numbers and ranges)."""
selected = set()
parts = selection_str.replace(' ', '').split(',')
for part in parts:
if '-' in part: # Handle range
try:
start, end = map(int, part.split('-'))
if not (1 <= start <= end <= max_members):
raise ValueError
selected.update(range(start, end + 1))
except ValueError:
print(f"Invalid range: {part}. Must be within 1-{max_members}.")
else: # Handle individual number
try:
num = int(part)
if not (1 <= num <= max_members):
raise ValueError
selected.add(num)
except ValueError:
print(f"Invalid number: {part}. Must be within 1-{max_members}.")
return sorted(list(selected))
async def add_member_to_db(db, member_id):
"""Adds a member ID to the database (handles duplicates)."""
try:
await db.execute("INSERT INTO added_members (member_id) VALUES (?)", (member_id,))
await db.commit()
except aiosqlite.IntegrityError:
pass # Ignore duplicate entries
async def check_if_member_added(db, member_id):
"""Checks if a member ID exists in the database."""
async with db.execute("SELECT 1 FROM added_members WHERE member_id = ?", (member_id,)) as cursor:
return await cursor.fetchone() is not None
async def add_members_with_client(client, target_group_id, members_to_add, db):
"""Adds members using a single client, handling errors."""
for member in members_to_add:
if await check_if_member_added(db, member['id']):
continue # Skip if already added
try:
user_to_add = await client.get_entity(member['username'])
await client(InviteToChannelRequest(target_group_id, [user_to_add]))
await add_member_to_db(db, member['id'])
successful_adds += 1
progress = (successful_adds / total_members) * 100
print(f"✓ Added {member['username']} using {client.session.filename} ({successful_adds}/{total_members} - {progress:.1f}%)")
await asyncio.sleep(DELAY_BETWEEN_ADDS) # Delay after success
except UserPrivacyRestrictedError:
print(f"× Privacy restricted: {member['username']}")
except UserIdInvalidError:
print(f"x Invalid user ID: {member['username']}")
except (FloodWaitError, PeerFloodError) as e:
wait_time = e.seconds if isinstance(e, FloodWaitError) else 600
print(f"! Flood wait ({client.session.filename}): {wait_time} seconds")
return wait_time # Return wait time
except Exception as e:
print(f"× Error ({client.session.filename}): {str(e)}")
return 0 # Return 0 if no FloodWait
async def main():
"""Main function to scrape and add members."""
os.makedirs(os.path.dirname(DATABASE_FILE), exist_ok=True) # Ensure db directory exists
async with aiosqlite.connect(DATABASE_FILE) as db:
await db.execute("CREATE TABLE IF NOT EXISTS added_members (member_id INTEGER PRIMARY KEY)")
await db.commit()
clients = []
for account in ACCOUNTS:
client = TelegramClient(f"session_{account['PHONE']}", account['API_ID'], account['API_HASH'])
await client.connect()
if not await client.is_user_authorized():
phone = account['PHONE']
print(f"Authorizing {phone}...")
await client.send_code_request(phone)
code = password_input(f"Enter code for {phone}: ")
try:
await client.sign_in(phone, code=code)
except SessionPasswordNeededError:
password = password_input(f"Enter 2FA password for {phone}: ")
await client.sign_in(password=password)
clients.append(client)
# Input: Source and Target Groups
print("\nEnter group usernames (e.g., @mygroup) or IDs (e.g., -1001234567890).")
while True:
source_group_input = input("Enter source group: ").replace('https://t.me/', '')
try:
source_group_entity = await client.get_entity(parse_group_identifier(source_group_input))
source_group_id = source_group_entity.id
print(f"Source group: {source_group_entity.title} (ID: {source_group_id})")
break
except ValueError as e:
print(f"Error: {e}")
except Exception as e:
print(f"Error getting source group: {e}")
sys.exit(1)
while True:
target_group_input = input("Enter target group: ").replace('https://t.me/', '')
try:
target_group_entity = await client.get_entity(parse_group_identifier(target_group_input))
target_group_id = target_group_entity.id
print(f"Target group: {target_group_entity.title} (ID: {target_group_id})")
break
except ValueError as e:
print(f"Error: {e}")
except Exception as e:
print(f"Error getting target group: {e}")
sys.exit(1)
# Scrape Members
print("\nScraping members...")
members = []
async for user in client.iter_participants(source_group_entity):
if user.bot or not user.username:
continue
members.append({
'username': user.username,
'id': user.id,
'name': f"{user.first_name} {user.last_name or ''}"
})
# Member Selection
print("\nScraped Members:")
for i, member in enumerate(members):
print(f"{i + 1}. {member['name']} (@{member['username']})")
print("\nEnter members to add (e.g., 1,3,5 / 1-5 / 1-5,8,11-13):")
while True:
selection = input("Selection: ")
selected_indices = parse_member_selection(selection, len(members))
if selected_indices:
break
print("No valid members selected. Try again.")
selected_members = [members[i - 1] for i in selected_indices]
# Add Members (Concurrent)
print("\nAdding members...")
print(f"Waiting for an initial delay of {INITIAL_DELAY} seconds...")
await asyncio.sleep(INITIAL_DELAY)
wait_until = {client.session.filename: 0 for client in clients}
members_per_batch = len(clients) # Dynamic batch size
total_members = len(selected_members)
successful_adds = 0
while selected_members:
tasks = []
now = time.time()
for i, client in enumerate(clients):
if now >= wait_until[client.session.filename]:
# Each client gets one member
if i < len(selected_members):
members_to_add = [selected_members[i]]
tasks.append(
asyncio.create_task(
add_members_with_client(client, target_group_id, members_to_add, db)
)
)
await asyncio.sleep(DELAY_BETWEEN_CONCURRENT) # Small delay
wait_until[client.session.filename] = now + DELAY_BETWEEN_ADDS
results = await asyncio.gather(*tasks)
for client, wait_time in zip(clients, results):
if wait_time > 0:
wait_until[client.session.filename] = now + wait_time
selected_members = selected_members[len(tasks):]
print("\nFinished adding members.")
# Cleanup
await db.close()
for client in clients:
await client.disconnect()
if __name__ == '__main__':
asyncio.run(main())