| Server IP : 3.147.158.171 / Your IP : 216.73.216.229 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/repo/api/scripts/ |
Upload File : |
from __future__ import annotations
"""
Register legacy characters in WordPress.
Finds characters in Drupal that have no corresponding WordPress author account
and registers them. Useful for characters created before registration was moved
to character-creation time.
Usage:
cd api
python scripts/register_characters.py --site guitar
python scripts/register_characters.py # all sites
python scripts/register_characters.py --site guitar --fix-emails # fix @example.com emails
python scripts/register_characters.py --backfill-uuids # backfill tsai_character_uuid meta on all sites
"""
import argparse
import logging
import secrets
import string
import sys
from pathlib import Path
API_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(API_DIR))
import requests
from dotenv import load_dotenv
load_dotenv(API_DIR / '.env')
from app.services.wp import WpService, USERS_URL
from app.services.site_config_service import SiteConfigService
from app.services.drupal import DrupalService
logging.basicConfig(
level=logging.INFO,
format='%(levelname)s %(asctime)s %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p',
)
logger = logging.getLogger(__name__)
def _random_password(length: int = 16) -> str:
return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(length))
def register_for_site(site, all_character_data, wp_service: WpService, drupal_service: DrupalService) -> tuple[int, int, int]:
"""Register unregistered characters for a site. Returns (registered, skipped, failed)."""
logger.info(f'[{site.slug}] Fetching WP users from {site.wp_host}')
token = wp_service.get_jwt_token(site)
if not token or isinstance(token, dict):
logger.error(f'[{site.slug}] Could not get JWT token — skipping')
return 0, 0, 0
wp_users = wp_service.get_users(site, token)
if wp_users is None:
logger.error(f'[{site.slug}] Could not fetch WP users — skipping')
return 0, 0, 0
existing_names = {u['name'] for u in wp_users}
logger.info(f'[{site.slug}] {len(existing_names)} existing WP users')
registered = 0
skipped = 0
failed = 0
for char_data in all_character_data:
character = drupal_service.create_character(None, char_data)
belongs_to_site = character.siteId == site.slug or character.systemWide
if not belongs_to_site:
continue
if character.name in existing_names:
logger.info(
f'[{site.slug}] {character.name} — already registered, skipping')
skipped += 1
continue
result = wp_service.register_user(
site=site,
token=token,
username=character.name,
email=character.email,
password=_random_password(),
first_name=character.name,
last_name=None,
roles=['author'],
)
if result['success']:
logger.info(
f'[{site.slug}] Registered {character.name} (WP ID {result["user_id"]})')
existing_names.add(character.name)
registered += 1
else:
logger.error(
f'[{site.slug}] Failed to register {character.name}: {result["message"]}')
failed += 1
return registered, skipped, failed
def fix_emails_for_site(site, all_character_data, wp_service: WpService, drupal_service: DrupalService) -> tuple[int, int, int]:
"""Update WP users whose email ends in @example.com to use the character's real email.
Returns (updated, skipped, failed)."""
logger.info(f'[{site.slug}] Fixing example.com emails at {site.wp_host}')
token = wp_service.get_jwt_token(site)
if not token or isinstance(token, dict):
logger.error(f'[{site.slug}] Could not get JWT token — skipping')
return 0, 0, 0
# Fetch users with edit context so the email field is included in the response
headers_auth = {'Authorization': f'Bearer {token}'}
users_response = requests.get(
f'{site.wp_host}{USERS_URL}?per_page=100&context=edit',
headers=headers_auth,
)
if users_response.status_code != 200:
logger.error(f'[{site.slug}] Could not fetch WP users — skipping')
return 0, 0, 0
wp_users = users_response.json()
# Build name → character map for this site
name_to_character = {}
for char_data in all_character_data:
character = drupal_service.create_character(None, char_data)
if character.siteId == site.slug or character.systemWide:
name_to_character[character.name] = character
headers = {'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'}
updated = 0
skipped = 0
failed = 0
for wp_user in wp_users:
if not wp_user.get('email', '').endswith('@example.com'):
skipped += 1
continue
character = name_to_character.get(wp_user['name'])
if not character:
logger.warning(
f'[{site.slug}] No Drupal character found for WP user "{wp_user["name"]}" — skipping')
skipped += 1
continue
url = f'{site.wp_host}{USERS_URL}/{wp_user["id"]}'
response = requests.post(url, headers=headers, json={
'email': character.email})
if response.status_code == 200:
logger.info(
f'[{site.slug}] Updated {wp_user["name"]} email → {character.email}')
updated += 1
else:
logger.error(
f'[{site.slug}] Failed to update {wp_user["name"]}: {response.status_code} {response.text}')
failed += 1
return updated, skipped, failed
def backfill_uuids_for_site(site, all_character_data, wp_service: WpService, drupal_service: DrupalService) -> tuple[int, int, int]:
"""Write each WP user's tsai_character_uuid meta to its Drupal character UUID.
Returns (updated, skipped, failed)."""
logger.info(f'[{site.slug}] Backfilling tsai_character_uuid at {site.wp_host}')
token = wp_service.get_jwt_token(site)
if not token or isinstance(token, dict):
logger.error(f'[{site.slug}] Could not get JWT token — skipping')
return 0, 0, 0
wp_users = wp_service.get_users(site, token)
if wp_users is None:
logger.error(f'[{site.slug}] Could not fetch WP users — skipping')
return 0, 0, 0
name_to_character = {}
for char_data in all_character_data:
character = drupal_service.create_character(None, char_data)
if character.siteId == site.slug or character.systemWide:
name_to_character[character.name] = character
headers = {'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'}
updated = 0
skipped = 0
failed = 0
for wp_user in wp_users:
# Only author-role users represent characters. Skip admins/editors/etc.
# to avoid e.g. a service user whose display name happens to match a
# Drupal character (the "Scike" service user vs. a "Scike" character).
roles = wp_user.get('roles') or []
if 'author' not in roles:
skipped += 1
continue
character = name_to_character.get(wp_user['name'])
if not character:
logger.warning(
f'[{site.slug}] No Drupal character found for WP user "{wp_user["name"]}" — skipping')
skipped += 1
continue
if not character.storageId:
logger.warning(
f'[{site.slug}] Character {character.name} has no storageId — skipping')
skipped += 1
continue
meta = wp_user.get('meta') if isinstance(wp_user, dict) else None
existing_uuid = ''
if isinstance(meta, dict):
existing_uuid = meta.get('tsai_character_uuid') or ''
if existing_uuid == character.storageId:
logger.info(
f'[{site.slug}] {character.name} — already has correct UUID, skipping')
skipped += 1
continue
url = f'{site.wp_host}{USERS_URL}/{wp_user["id"]}'
response = requests.post(url, headers=headers, json={
'meta': {'tsai_character_uuid': character.storageId}})
if response.status_code in (200, 201):
logger.info(
f'[{site.slug}] Set tsai_character_uuid={character.storageId} on {character.name} (WP ID {wp_user["id"]})')
updated += 1
else:
logger.error(
f'[{site.slug}] Failed to set UUID on {character.name}: {response.status_code} {response.text}')
failed += 1
return updated, skipped, failed
def main() -> int:
parser = argparse.ArgumentParser(
description='Register unregistered Drupal characters in WordPress')
parser.add_argument(
'--site', help='Site slug to process (default: all sites)')
mode = parser.add_mutually_exclusive_group()
mode.add_argument('--fix-emails', action='store_true',
help='Update existing WP users that have @example.com emails')
mode.add_argument('--backfill-uuids', action='store_true',
help='Write tsai_character_uuid meta on existing WP users')
args = parser.parse_args()
site_service = SiteConfigService()
wp_service = WpService()
drupal_service = DrupalService()
if args.site:
site = site_service.get_site_by_slug(args.site)
if not site:
logger.error(f'Unknown site slug: {args.site}')
return 1
sites = [site]
else:
sites = site_service.get_all_sites()
logger.info(f'Fetching all characters from Drupal...')
all_character_data = drupal_service.fetch_all_characters()
logger.info(f'Found {len(all_character_data)} total characters in Drupal')
if args.fix_emails:
label_a = 'updated'
elif args.backfill_uuids:
label_a = 'backfilled'
else:
label_a = 'registered'
total_a = 0
total_skipped = 0
total_failed = 0
for site in sites:
if args.fix_emails:
a, s, f = fix_emails_for_site(
site, all_character_data, wp_service, drupal_service)
elif args.backfill_uuids:
a, s, f = backfill_uuids_for_site(
site, all_character_data, wp_service, drupal_service)
else:
a, s, f = register_for_site(
site, all_character_data, wp_service, drupal_service)
total_a += a
total_skipped += s
total_failed += f
logger.info(
f'Done — {label_a}: {total_a}, skipped: {total_skipped}, failed: {total_failed}')
return 0 if total_failed == 0 else 1
if __name__ == '__main__':
sys.exit(main())