From cd1115ab6824c532ebe1d30b5b8a2de3c57fa40d Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Wed, 10 Jun 2026 09:01:43 +0700 Subject: [PATCH] feat(redis): add phone key migration script --- migrate_phone_keys_to_new_format.py | 152 ++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 migrate_phone_keys_to_new_format.py diff --git a/migrate_phone_keys_to_new_format.py b/migrate_phone_keys_to_new_format.py new file mode 100644 index 0000000..050b2be --- /dev/null +++ b/migrate_phone_keys_to_new_format.py @@ -0,0 +1,152 @@ +"""Migrate old phone-scoped Redis keys to the current phone-less key format. + +Old: + telegram-export:session: + telegram-export:run::: + +New: + telegram-export:session + telegram-export:run:: +""" + +import argparse +import json +import os + +import redis +from dotenv import load_dotenv + +PREFIX = 'telegram-export' + + +def redis_key(*parts): + return ':'.join([PREFIX, *parts]) + + +def parse_args(): + parser = argparse.ArgumentParser( + description='Migrate Redis keys from phone-scoped format to phone-less format.', + ) + parser.add_argument( + '--phone', + help='Phone part in old Redis keys. Defaults to telegram-export:config phone.', + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Print planned changes without writing Redis.', + ) + parser.add_argument( + '--overwrite', + action='store_true', + help='Overwrite new-format keys when they already exist.', + ) + parser.add_argument( + '--delete-old', + action='store_true', + help='Delete old phone-scoped keys after successful copy.', + ) + return parser.parse_args() + + +def connect_redis(): + load_dotenv() + redis_url = os.getenv('REDIS_URL') + if not redis_url: + raise SystemExit('REDIS_URL not set in .env') + return redis.from_url(redis_url, decode_responses=True) + + +def resolve_phone(client, phone): + if phone: + return phone + + raw_config = client.get(redis_key('config')) + if not raw_config: + raise SystemExit('No config found. Pass --phone explicitly.') + + try: + config = json.loads(raw_config) + except ValueError as exc: + raise SystemExit('telegram-export:config is not valid JSON.') from exc + + config_phone = config.get('phone') + if not config_phone: + raise SystemExit('Config has no phone. Pass --phone explicitly.') + return str(config_phone) + + +def migrate_key(client, old_key, new_key, args): + value = client.get(old_key) + if value is None: + return 'missing' + + if client.exists(new_key) and not args.overwrite: + print(f'skip existing: {new_key}') + return 'skipped' + + print(f'{old_key} -> {new_key}') + if args.dry_run: + return 'planned' + + client.set(new_key, value) + if args.delete_old: + client.delete(old_key) + return 'migrated' + + +def migrate_session(client, phone, args): + return migrate_key( + client, + redis_key('session', phone), + redis_key('session'), + args, + ) + + +def migrate_runs(client, phone, args): + counts = {'planned': 0, 'migrated': 0, 'skipped': 0, 'missing': 0} + pattern = redis_key('run', phone, '*') + + for old_key in client.scan_iter(match=pattern): + parts = old_key.split(':') + if len(parts) != 5: + print(f'skip invalid old run key: {old_key}') + counts['skipped'] += 1 + continue + + _, _, _, run_time, group_id = parts + result = migrate_key( + client, + old_key, + redis_key('run', run_time, group_id), + args, + ) + counts[result] += 1 + + return counts + + +def main(): + args = parse_args() + client = connect_redis() + phone = resolve_phone(client, args.phone) + + print(f'migrating phone-scoped keys for {phone}') + session_result = migrate_session(client, phone, args) + run_counts = migrate_runs(client, phone, args) + + print('') + print(f'session: {session_result}') + print( + 'runs: ' + f'{run_counts["migrated"]} migrated, ' + f'{run_counts["planned"]} planned, ' + f'{run_counts["skipped"]} skipped' + ) + if args.dry_run: + print('dry run only; no Redis keys changed.') + + +if __name__ == '__main__': + raise SystemExit(main())