mirror of
https://github.com/tiennm99/export-telegram-group-members.git
synced 2026-10-15 12:14:10 +00:00
feat: add crawl comparison command
This commit is contained in:
1 parent
834ad56f65
commit
fb9384d965
5 files changed
+184
-2
No files matched your search
@@ -19,14 +19,30 @@ pip install -r requirements.txt
|
||||
3. Create a new Telegram app at [https://my.telegram.org](https://my.telegram.org) and get the `api_id` and `api_hash`.
|
||||
4. Create a free Redis database (e.g. [Upstash](https://upstash.com)) and copy its `rediss://` connection URL.
|
||||
5. Copy `.env.example` to `.env` and fill in `API_ID`, `API_HASH`, `PHONE`, `GROUP_IDS`, and `REDIS_URL`.
|
||||
6. Run the script:
|
||||
6. Crawl configured groups:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
python crawl.py
|
||||
```
|
||||
|
||||
The first run 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 that session and **does not** prompt again.
|
||||
|
||||
## Compare two crawls
|
||||
|
||||
Compare membership changes for one group between two saved crawls:
|
||||
|
||||
```bash
|
||||
python compare.py <group_id> <time1> <time2>
|
||||
```
|
||||
|
||||
If `time1` and `time2` are omitted, the command compares the latest two crawls for that group:
|
||||
|
||||
```bash
|
||||
python compare.py <group_id>
|
||||
```
|
||||
|
||||
The output lists members added in `time2` and removed since `time1`.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Description |
|
||||
|
||||
@@ -60,3 +60,50 @@ def list_exports():
|
||||
continue
|
||||
records.sort(key=lambda r: (r.get('time', ''), r.get('group_id', 0)))
|
||||
return records
|
||||
|
||||
|
||||
def list_group_exports(group_id):
|
||||
"""Return export records for one group, sorted by run time."""
|
||||
group_id = str(group_id)
|
||||
return [
|
||||
record for record in list_exports()
|
||||
if str(record.get('group_id')) == group_id
|
||||
]
|
||||
|
||||
|
||||
def get_group_export(group_id, run_time):
|
||||
"""Return a group export at one run time, or None when missing."""
|
||||
for record in list_group_exports(group_id):
|
||||
if record.get('time') == run_time:
|
||||
return record
|
||||
return None
|
||||
|
||||
|
||||
def latest_two_group_exports(group_id):
|
||||
"""Return the latest two export records for a group."""
|
||||
exports = list_group_exports(group_id)
|
||||
if len(exports) < 2:
|
||||
return None, None
|
||||
return exports[-2], exports[-1]
|
||||
|
||||
|
||||
def diff_group_members(before_record, after_record):
|
||||
"""Compare two group export records by Telegram member id."""
|
||||
before_members = _member_index(before_record)
|
||||
after_members = _member_index(after_record)
|
||||
|
||||
before_ids = set(before_members)
|
||||
after_ids = set(after_members)
|
||||
|
||||
added = [after_members[member_id] for member_id in sorted(after_ids - before_ids)]
|
||||
removed = [before_members[member_id] for member_id in sorted(before_ids - after_ids)]
|
||||
return added, removed
|
||||
|
||||
|
||||
def _member_index(record):
|
||||
members = {}
|
||||
for member in record.get('members', []):
|
||||
member_id = member.get('id')
|
||||
if member_id is not None:
|
||||
members[int(member_id)] = member
|
||||
return members
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from common import diff_group_members, get_group_export, latest_two_group_exports
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Compare member changes between two crawls for one group.',
|
||||
)
|
||||
parser.add_argument('group_id', type=int, help='Telegram group id to compare')
|
||||
parser.add_argument(
|
||||
'times',
|
||||
nargs='*',
|
||||
help='Optional pair: time1 time2. When omitted, latest two crawls are used.',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if len(args.times) not in (0, 2):
|
||||
parser.error('provide both time1 and time2, or omit both to compare latest two crawls')
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
if args.times:
|
||||
before_time, after_time = args.times
|
||||
before_record = get_group_export(args.group_id, before_time)
|
||||
after_record = get_group_export(args.group_id, after_time)
|
||||
if before_record is None:
|
||||
print(f'export not found for group {args.group_id} at {before_time}', file=sys.stderr)
|
||||
return 1
|
||||
if after_record is None:
|
||||
print(f'export not found for group {args.group_id} at {after_time}', file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
before_record, after_record = latest_two_group_exports(args.group_id)
|
||||
if before_record is None or after_record is None:
|
||||
print(f'need at least 2 exports for group {args.group_id}', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print_summary(args.group_id, before_record, after_record)
|
||||
return 0
|
||||
|
||||
|
||||
def print_summary(group_id, before_record, after_record):
|
||||
added, removed = diff_group_members(before_record, after_record)
|
||||
title = after_record.get('title') or before_record.get('title') or ''
|
||||
before_members = before_record.get('members', [])
|
||||
after_members = after_record.get('members', [])
|
||||
|
||||
print(f'group: {title} ({group_id})' if title else f'group: {group_id}')
|
||||
print(f'before: {before_record.get("time")} ({len(before_members)} members)')
|
||||
print(f'after: {after_record.get("time")} ({len(after_members)} members)')
|
||||
print()
|
||||
|
||||
print(f'added: {len(added)}')
|
||||
for member in added:
|
||||
print(f'+ {format_member(member)}')
|
||||
|
||||
print()
|
||||
print(f'removed: {len(removed)}')
|
||||
for member in removed:
|
||||
print(f'- {format_member(member)}')
|
||||
|
||||
if not added and not removed:
|
||||
print()
|
||||
print('no membership changes.')
|
||||
|
||||
|
||||
def format_member(member):
|
||||
parts = [str(member.get('id'))]
|
||||
username = member.get('username')
|
||||
full_name = ' '.join(
|
||||
part for part in [member.get('first_name'), member.get('last_name')]
|
||||
if part
|
||||
)
|
||||
if username:
|
||||
parts.append(f'@{username}')
|
||||
if full_name:
|
||||
parts.append(full_name)
|
||||
return ' | '.join(parts)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
File renamed without changes.
@@ -0,0 +1,33 @@
|
||||
# Compare Group Export Diffs
|
||||
|
||||
## Context
|
||||
|
||||
- `common.py` stores and lists Redis export records.
|
||||
- `crawl.py` exports groups and should not be reused for comparison because it logs into Telegram.
|
||||
- `README.md` documents current usage and Redis record shape.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add command that receives `group_id`, optional `time1`, optional `time2`.
|
||||
- If both times missing, compare latest 2 exports for that group.
|
||||
- If one time missing, fail with clear usage error.
|
||||
- Print members added and removed between two crawls.
|
||||
- Keep Redis schema unchanged.
|
||||
|
||||
## Implementation
|
||||
|
||||
1. Add focused helpers in `common.py` to filter exports by group/time and compare members by Telegram `id`.
|
||||
2. Add `compare.py` CLI with argparse.
|
||||
3. Update `README.md` with command examples.
|
||||
4. Run Python compile check.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- `python compare.py <group_id>` compares latest two times.
|
||||
- `python compare.py <group_id> <time1> <time2>` compares explicit times.
|
||||
- Missing export records produce readable errors.
|
||||
- Existing export flow remains unchanged.
|
||||
|
||||
## Unresolved Questions
|
||||
|
||||
None.
|
||||
Reference in new issue
Block a user