Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
249 lines
9.3 KiB
Python
249 lines
9.3 KiB
Python
"""
|
|
Operation 2: Move unique contacts from Temp to Main Contacts folder (278 contacts)
|
|
Tries move endpoint first, falls back to copy+delete.
|
|
"""
|
|
import json
|
|
import subprocess
|
|
import time
|
|
import urllib.parse
|
|
|
|
TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f"
|
|
CLIENT_ID = "fabb3421-8b34-484b-bc17-e46de9703418"
|
|
CLIENT_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO"
|
|
SCOPE = "https://graph.microsoft.com/.default"
|
|
USER = "barbara@bardach.net"
|
|
DATA_FILE = "D:/ClaudeTools/temp/bardach_temp_vs_main.json"
|
|
OUTPUT_FILE = "D:/ClaudeTools/temp/bardach_op2_move_unique.json"
|
|
|
|
# Contact fields to copy in fallback mode
|
|
CONTACT_FIELDS = [
|
|
"givenName", "surname", "displayName", "middleName", "nickName",
|
|
"title", "jobTitle", "companyName", "department", "officeLocation",
|
|
"businessHomePage", "personalNotes", "generation", "imAddresses",
|
|
"emailAddresses", "homePhones", "mobilePhone", "businessPhones",
|
|
"homeAddress", "businessAddress", "otherAddress",
|
|
"birthday", "yomiGivenName", "yomiSurname", "yomiCompanyName",
|
|
"fileAs", "initials", "manager", "assistantName", "profession",
|
|
"spouseName", "children"
|
|
]
|
|
|
|
def get_token():
|
|
url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
|
|
data = (
|
|
f"client_id={CLIENT_ID}"
|
|
f"&scope={urllib.parse.quote(SCOPE)}"
|
|
f"&client_secret={urllib.parse.quote(CLIENT_SECRET)}"
|
|
f"&grant_type=client_credentials"
|
|
)
|
|
result = subprocess.run(
|
|
["curl", "-s", "-X", "POST", url,
|
|
"-H", "Content-Type: application/x-www-form-urlencoded",
|
|
"-d", data],
|
|
capture_output=True, text=True
|
|
)
|
|
resp = json.loads(result.stdout)
|
|
if "access_token" not in resp:
|
|
print(f"[ERROR] Token acquisition failed: {resp}")
|
|
raise Exception("Failed to get token")
|
|
print("[OK] Token acquired")
|
|
return resp["access_token"]
|
|
|
|
def get_contacts_folder_id(token):
|
|
"""Get the default Contacts folder ID."""
|
|
url = f"https://graph.microsoft.com/v1.0/users/{USER}/contactFolders"
|
|
result = subprocess.run(
|
|
["curl", "-s", "-X", "GET", url,
|
|
"-H", f"Authorization: Bearer {token}"],
|
|
capture_output=True, text=True
|
|
)
|
|
resp = json.loads(result.stdout)
|
|
for folder in resp.get("value", []):
|
|
if folder.get("displayName") == "Contacts":
|
|
return folder["id"]
|
|
return None
|
|
|
|
def try_move(token, contact_id, dest_id):
|
|
"""Try the /move endpoint."""
|
|
url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}/move"
|
|
body = json.dumps({"destinationId": dest_id})
|
|
result = subprocess.run(
|
|
["curl", "-s", "-w", "\n%{http_code}", "-X", "POST", url,
|
|
"-H", f"Authorization: Bearer {token}",
|
|
"-H", "Content-Type: application/json",
|
|
"-d", body],
|
|
capture_output=True, text=True
|
|
)
|
|
lines = result.stdout.rsplit("\n", 1)
|
|
status = lines[-1].strip() if len(lines) > 1 else "000"
|
|
body_text = lines[0] if len(lines) > 1 else result.stdout
|
|
return status, body_text
|
|
|
|
def get_contact(token, contact_id):
|
|
"""Read full contact data."""
|
|
url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}"
|
|
result = subprocess.run(
|
|
["curl", "-s", "-X", "GET", url,
|
|
"-H", f"Authorization: Bearer {token}"],
|
|
capture_output=True, text=True
|
|
)
|
|
return json.loads(result.stdout)
|
|
|
|
def create_contact(token, contact_data):
|
|
"""Create a contact in the default Contacts folder."""
|
|
url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts"
|
|
# Build clean payload with only writable fields
|
|
payload = {}
|
|
for field in CONTACT_FIELDS:
|
|
val = contact_data.get(field)
|
|
if val is not None and val != "" and val != [] and val != {}:
|
|
payload[field] = val
|
|
body = json.dumps(payload)
|
|
result = subprocess.run(
|
|
["curl", "-s", "-w", "\n%{http_code}", "-X", "POST", url,
|
|
"-H", f"Authorization: Bearer {token}",
|
|
"-H", "Content-Type: application/json",
|
|
"-d", body],
|
|
capture_output=True, text=True
|
|
)
|
|
lines = result.stdout.rsplit("\n", 1)
|
|
status = lines[-1].strip() if len(lines) > 1 else "000"
|
|
body_text = lines[0] if len(lines) > 1 else result.stdout
|
|
return status, body_text
|
|
|
|
def delete_contact(token, contact_id):
|
|
url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}"
|
|
result = subprocess.run(
|
|
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
|
"-X", "DELETE", url,
|
|
"-H", f"Authorization: Bearer {token}"],
|
|
capture_output=True, text=True
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print("OPERATION 2: Move unique contacts to Main Contacts (278)")
|
|
print("=" * 60)
|
|
|
|
with open(DATA_FILE, "r") as f:
|
|
data = json.load(f)
|
|
|
|
unique = data["unique_to_temp"]
|
|
total = len(unique)
|
|
print(f"[INFO] Loaded {total} unique contacts to move")
|
|
|
|
token = get_token()
|
|
|
|
# Get the contacts folder ID for move endpoint
|
|
folder_id = get_contacts_folder_id(token)
|
|
print(f"[INFO] Main Contacts folder ID: {folder_id}")
|
|
|
|
# Test move endpoint with first contact
|
|
move_works = False
|
|
if total > 0:
|
|
test_id = unique[0]["temp_id"]
|
|
# Try with "contacts" string first
|
|
status, body = try_move(token, test_id, "contacts")
|
|
if status in ("200", "201"):
|
|
move_works = True
|
|
print("[OK] Move endpoint works with 'contacts' destination")
|
|
elif folder_id:
|
|
# Try with actual folder ID
|
|
status, body = try_move(token, test_id, folder_id)
|
|
if status in ("200", "201"):
|
|
move_works = True
|
|
print("[OK] Move endpoint works with folder ID")
|
|
else:
|
|
print(f"[WARNING] Move endpoint returned {status}, falling back to copy+delete")
|
|
print(f" Response: {body[:200]}")
|
|
else:
|
|
print(f"[WARNING] Move returned {status} and no folder ID found, using copy+delete")
|
|
|
|
use_move = move_works
|
|
dest_id = "contacts" if not folder_id else folder_id
|
|
# If the first contact was already moved successfully via test, track it
|
|
start_index = 1 if move_works else 0
|
|
|
|
successes = []
|
|
failures = []
|
|
method_used = "move" if use_move else "copy+delete"
|
|
print(f"[INFO] Using method: {method_used}")
|
|
|
|
if move_works:
|
|
# First one already moved
|
|
successes.append({
|
|
"temp_id": unique[0]["temp_id"],
|
|
"displayName": unique[0].get("displayName", ""),
|
|
"method": "move"
|
|
})
|
|
|
|
start_time = time.time()
|
|
|
|
for i in range(start_index, total):
|
|
contact = unique[i]
|
|
# Re-acquire token every 250 operations
|
|
if i > 0 and i % 250 == 0:
|
|
print(f"[INFO] Re-acquiring token at operation {i}...")
|
|
token = get_token()
|
|
|
|
temp_id = contact["temp_id"]
|
|
display = contact.get("displayName", "")
|
|
|
|
if use_move:
|
|
status, body = try_move(token, temp_id, dest_id)
|
|
if status in ("200", "201"):
|
|
successes.append({"temp_id": temp_id, "displayName": display, "method": "move"})
|
|
else:
|
|
failures.append({"temp_id": temp_id, "displayName": display, "status": status, "error": body[:200]})
|
|
else:
|
|
# Fallback: copy + delete
|
|
try:
|
|
cdata = get_contact(token, temp_id)
|
|
if "error" in cdata:
|
|
failures.append({"temp_id": temp_id, "displayName": display, "status": "read_fail", "error": str(cdata["error"])[:200]})
|
|
continue
|
|
|
|
cstatus, cbody = create_contact(token, cdata)
|
|
if cstatus in ("200", "201"):
|
|
# Delete original
|
|
dstatus = delete_contact(token, temp_id)
|
|
if dstatus in ("204", "200"):
|
|
successes.append({"temp_id": temp_id, "displayName": display, "method": "copy+delete"})
|
|
else:
|
|
successes.append({"temp_id": temp_id, "displayName": display, "method": "copy_only", "delete_status": dstatus})
|
|
else:
|
|
failures.append({"temp_id": temp_id, "displayName": display, "status": cstatus, "error": cbody[:200]})
|
|
except Exception as e:
|
|
failures.append({"temp_id": temp_id, "displayName": display, "status": "exception", "error": str(e)[:200]})
|
|
|
|
# Progress every 25
|
|
if (i + 1) % 25 == 0 or (i + 1) == total:
|
|
elapsed = time.time() - start_time
|
|
rate = (i + 1) / elapsed if elapsed > 0 else 0
|
|
print(f" [{i+1}/{total}] OK={len(successes)} FAIL={len(failures)} ({rate:.1f}/sec)")
|
|
|
|
elapsed = time.time() - start_time
|
|
results = {
|
|
"operation": "move_unique_contacts",
|
|
"method": method_used,
|
|
"total": total,
|
|
"successes": len(successes),
|
|
"failures": len(failures),
|
|
"elapsed_seconds": round(elapsed, 1),
|
|
"successful_contacts": successes,
|
|
"failed_contacts": failures
|
|
}
|
|
|
|
with open(OUTPUT_FILE, "w") as f:
|
|
json.dump(results, f, indent=2)
|
|
|
|
print(f"\n[SUCCESS] Operation 2 complete")
|
|
print(f" Moved: {len(successes)}/{total}")
|
|
print(f" Failed: {len(failures)}/{total}")
|
|
print(f" Method: {method_used}")
|
|
print(f" Time: {elapsed:.1f}s")
|
|
print(f" Results: {OUTPUT_FILE}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|