148 lines
5.9 KiB
Python
148 lines
5.9 KiB
Python
import urllib.request, urllib.parse, json, os
|
|
from collections import defaultdict
|
|
|
|
APP_ID = "fabb3421-8b34-484b-bc17-e46de9703418"
|
|
TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f"
|
|
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
|
|
USER_ID = "41d14430-feb4-4ae2-aed6-2bd4e6384ca7"
|
|
|
|
# Get token
|
|
token_data = urllib.parse.urlencode({
|
|
'client_id': APP_ID,
|
|
'client_secret': CLIENT_SECRET,
|
|
'scope': 'https://graph.microsoft.com/.default',
|
|
'grant_type': 'client_credentials'
|
|
}).encode()
|
|
req = urllib.request.Request(f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", data=token_data, method='POST')
|
|
with urllib.request.urlopen(req) as r:
|
|
token = json.loads(r.read())['access_token']
|
|
|
|
base = f'https://graph.microsoft.com/v1.0/users/{USER_ID}/contacts'
|
|
|
|
def patch_contact(cid, data):
|
|
body = json.dumps(data).encode()
|
|
req = urllib.request.Request(f'{base}/{cid}', data=body, method='PATCH',
|
|
headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'})
|
|
with urllib.request.urlopen(req) as r:
|
|
return r.status
|
|
|
|
def delete_contact(cid):
|
|
req = urllib.request.Request(f'{base}/{cid}', method='DELETE',
|
|
headers={'Authorization': f'Bearer {token}'})
|
|
with urllib.request.urlopen(req) as r:
|
|
return r.status
|
|
|
|
# Fetch all contacts
|
|
url = f'{base}?$select=id,displayName,emailAddresses,companyName,businessPhones,mobilePhone,jobTitle,givenName,surname&$orderby=displayName&$top=999'
|
|
all_contacts = []
|
|
while url:
|
|
req = urllib.request.Request(url, headers={'Authorization': f'Bearer {token}'})
|
|
with urllib.request.urlopen(req) as r:
|
|
data = json.loads(r.read())
|
|
all_contacts.extend(data.get('value', []))
|
|
url = data.get('@odata.nextLink')
|
|
print(f'Total contacts: {len(all_contacts)}')
|
|
|
|
by_name = defaultdict(list)
|
|
for c in all_contacts:
|
|
name = c.get('displayName', '').strip()
|
|
if name:
|
|
by_name[name].append(c)
|
|
|
|
dupes = {k: v for k, v in by_name.items() if len(v) > 1}
|
|
print(f'Duplicate groups: {len(dupes)}')
|
|
|
|
def merge_emails(keeper, donor):
|
|
keeper_emails = set(e.get('address', '').lower() for e in keeper.get('emailAddresses', []) if e.get('address', '').strip())
|
|
new_emails = [e for e in keeper.get('emailAddresses', []) if e.get('address', '').strip()]
|
|
added = []
|
|
for e in donor.get('emailAddresses', []):
|
|
addr = e.get('address', '')
|
|
if addr.strip() and addr.lower() not in keeper_emails:
|
|
new_emails.append(e)
|
|
added.append(addr)
|
|
return new_emails, added
|
|
|
|
def merge_phones(keeper, donor):
|
|
def normalize(p):
|
|
return ''.join(c for c in p if c.isdigit())[-10:]
|
|
keeper_phones = set()
|
|
for p in (keeper.get('businessPhones') or []):
|
|
keeper_phones.add(normalize(p))
|
|
if keeper.get('mobilePhone'):
|
|
keeper_phones.add(normalize(keeper['mobilePhone']))
|
|
new_phones = []
|
|
for p in (donor.get('businessPhones') or []):
|
|
if normalize(p) not in keeper_phones:
|
|
new_phones.append(p)
|
|
if donor.get('mobilePhone') and normalize(donor['mobilePhone']) not in keeper_phones:
|
|
new_phones.append(donor['mobilePhone'])
|
|
return new_phones
|
|
|
|
def do_merge(name, keeper, donor):
|
|
new_emails, added_emails = merge_emails(keeper, donor)
|
|
new_phones = merge_phones(keeper, donor)
|
|
patch = {}
|
|
if added_emails:
|
|
patch['emailAddresses'] = new_emails
|
|
if new_phones:
|
|
biz = list(keeper.get('businessPhones') or []) + new_phones
|
|
patch['businessPhones'] = biz
|
|
if not keeper.get('companyName') and donor.get('companyName'):
|
|
patch['companyName'] = donor['companyName']
|
|
if not keeper.get('jobTitle') and donor.get('jobTitle'):
|
|
patch['jobTitle'] = donor['jobTitle']
|
|
if patch:
|
|
status = patch_contact(keeper['id'], patch)
|
|
extras = []
|
|
if added_emails: extras.append(f"emails: {added_emails}")
|
|
if new_phones: extras.append(f"phones: {new_phones}")
|
|
if 'companyName' in patch: extras.append(f"company: {patch['companyName']}")
|
|
if 'jobTitle' in patch: extras.append(f"job: {patch['jobTitle']}")
|
|
print(f' [OK] {name}: merged {", ".join(extras)} (status {status})')
|
|
else:
|
|
print(f' [OK] {name}: no new data to merge')
|
|
del_status = delete_contact(donor['id'])
|
|
print(f' Deleted duplicate (status {del_status})')
|
|
|
|
# === EXACT DUPLICATES ===
|
|
print('\n--- EXACT DUPLICATES ---')
|
|
for name in ['Bardach, Mike', 'Brandon Lopez', 'Judi Carroll', 'Kelly Yang', 'Megan Carroll', 'Winter Williams']:
|
|
contacts = dupes[name]
|
|
for c in contacts[1:]:
|
|
try:
|
|
status = delete_contact(c['id'])
|
|
print(f' [OK] Deleted: {name} (status {status})')
|
|
except Exception as e:
|
|
print(f' [ERROR] {name}: {e}')
|
|
|
|
# === PATSY SABLE (3 copies) ===
|
|
print('\n--- Patsy Sable (3 copies) ---')
|
|
patsy = dupes['Patsy Sable']
|
|
patsy_personal = [c for c in patsy if any(e.get('address', '') == 'patsy@patsysable.com' for e in c.get('emailAddresses', []))]
|
|
patsy_work = [c for c in patsy if any(e.get('address', '') == 'psable@longrealty.com' for e in c.get('emailAddresses', []))]
|
|
if len(patsy_work) >= 2:
|
|
try:
|
|
status = delete_contact(patsy_work[1]['id'])
|
|
print(f' [OK] Deleted exact work dupe (status {status})')
|
|
except Exception as e:
|
|
print(f' [ERROR] work dupe: {e}')
|
|
if patsy_personal and patsy_work:
|
|
try:
|
|
do_merge('Patsy Sable', patsy_personal[0], patsy_work[0])
|
|
except Exception as e:
|
|
print(f' [ERROR] merge: {e}')
|
|
|
|
# === MERGE PAIRS ===
|
|
print('\n--- MERGE PAIRS ---')
|
|
for name in ['Barbara Bardach', 'David Rodriguez', 'Denise Newton', 'Gina Beltran',
|
|
'Jessica Bonn', 'Kayla Manley', 'Maria Anemone', 'Mark Crager',
|
|
'Paula Williams', 'Randy Bonn', 'Susan Barry']:
|
|
contacts = dupes[name]
|
|
try:
|
|
do_merge(name, contacts[0], contacts[1])
|
|
except Exception as e:
|
|
print(f' [ERROR] {name}: {e}')
|
|
|
|
print('\n=== ALL DONE ===')
|