feat(crawl): require explicit group ids

This commit is contained in:
tiennm99 committed 2026-07-24 16:38:40 +07:00
1 parent 72e16da6f7
commit 2543260a50
3 files changed
+131 -7

No files matched your search

+15 -4
View File
@@ -25,15 +25,26 @@ pip install -r requirements.txt
python configure.py
```
`configure.py` prompts for `api_id`, `api_hash`, phone, and group IDs.
`configure.py` prompts for `api_id`, `api_hash`, phone, and group IDs. The stored
group IDs remain available to commands that use configured defaults, such as
`compare.py`.
7. Crawl configured groups:
7. Crawl one or more groups by ID:
```bash
python crawl.py
python crawl.py <group_id> [<group_id> ...]
```
The first crawl asks for the Telegram login code once, then stores the session in Redis. Any later run — on any device pointed at the same Redis — reuses the Redis config and session, and **does not** prompt again.
For example:
```bash
python crawl.py -1001234567890 -1009876543210
```
Only the group IDs supplied to this command are crawled; the stored group list
is not used. The first crawl asks for the Telegram login code once, then stores
the session in Redis. Any later run — on any device pointed at the same Redis —
reuses the Redis config and session, and **does not** prompt again.
## Compare two crawls
+18 -3
View File
@@ -1,3 +1,4 @@
import argparse
import getpass
from telethon.errors import SessionPasswordNeededError
@@ -10,11 +11,25 @@ from config import load_app_config
from session_store import load_session, save_session
def main():
def parse_args(argv=None):
parser = argparse.ArgumentParser(
description='Export members from one or more Telegram groups.',
)
parser.add_argument(
'group_ids',
nargs='+',
type=int,
metavar='group_id',
help='Telegram group ID to crawl (provide one or more)',
)
return parser.parse_args(argv)
def main(argv=None):
args = parse_args(argv)
app_config = load_app_config()
api_hash = app_config['api_hash']
api_id = app_config['api_id']
group_ids = app_config['group_ids']
phone = app_config['phone']
# Session loads from Redis: a saved string means no re-login on any device.
@@ -32,7 +47,7 @@ def main():
run_time = new_run_time()
saved = 0
for group_id in group_ids:
for group_id in args.group_ids:
try:
entity = client.get_entity(group_id)
if not isinstance(entity, (Chat, Channel)):
+98
View File
@@ -0,0 +1,98 @@
import os
import unittest
from unittest.mock import MagicMock, call, patch
os.environ.setdefault('REDIS_URL', 'redis://localhost')
import crawl
class FakeChat:
def __init__(self, group_id):
self.id = group_id
self.title = f'Group {group_id}'
class FakeMember:
def __init__(self, member_id):
self.id = member_id
class CrawlArgumentsTest(unittest.TestCase):
def test_accepts_multiple_group_ids(self):
args = crawl.parse_args(['-100123', '-100456'])
self.assertEqual(args.group_ids, [-100123, -100456])
def test_requires_at_least_one_group_id(self):
with (
patch.object(crawl, 'load_app_config') as load_app_config,
patch.object(crawl, 'TelegramClient') as telegram_client,
self.assertRaises(SystemExit),
):
crawl.main([])
load_app_config.assert_not_called()
telegram_client.assert_not_called()
def test_rejects_non_integer_group_id(self):
with (
patch.object(crawl, 'load_app_config') as load_app_config,
patch.object(crawl, 'TelegramClient') as telegram_client,
self.assertRaises(SystemExit),
):
crawl.main(['not-a-group'])
load_app_config.assert_not_called()
telegram_client.assert_not_called()
class CrawlMainTest(unittest.TestCase):
@patch.object(crawl, 'save_group_export')
@patch.object(crawl, 'save_session')
@patch.object(crawl, 'load_session', return_value='stored-session')
@patch.object(crawl, 'load_app_config')
@patch.object(crawl, 'TelegramClient')
def test_crawls_supplied_ids_instead_of_configured_ids(
self,
telegram_client,
load_app_config,
load_session,
save_session,
save_group_export,
):
load_app_config.return_value = {
'api_id': 123,
'api_hash': 'hash',
'phone': '+15551234567',
'group_ids': [999],
}
client = MagicMock()
telegram_client.return_value = client
client.is_user_authorized.return_value = True
client.session.save.return_value = 'updated-session'
client.get_entity.side_effect = [FakeChat(101), FakeChat(202)]
client.get_participants.side_effect = [
[FakeMember(2), FakeMember(1)],
[FakeMember(4), FakeMember(3)],
]
with (
patch.object(crawl, 'Chat', FakeChat),
patch.object(crawl, 'Channel', FakeChat),
patch.object(crawl, 'StringSession', return_value='session'),
patch.object(crawl, 'new_run_time', return_value='20260724120000'),
):
result = crawl.main(['101', '202'])
self.assertEqual(result, 0)
self.assertEqual(client.get_entity.call_args_list, [call(101), call(202)])
self.assertNotIn(call(999), client.get_entity.call_args_list)
save_session.assert_called_once_with('updated-session')
self.assertEqual(save_group_export.call_count, 2)
first_members = save_group_export.call_args_list[0].args[2]
self.assertEqual([member.id for member in first_members], [1, 2])
if __name__ == '__main__':
unittest.main()