fix: Correct Novi API auth header and field names in Mailman bridge

- Auth: ApiKey header → Authorization: Basic (confirmed from IDAA Jitsi code)
- Member fields: confirmed PascalCase (FirstName, LastName, Email) from Novi API
- email.replace(' ', '+') to match Jitsi's sanitization pattern
- Bulk member list endpoint marked TODO pending confirmation
- Response unwrapping handles Results/Members/value/array shapes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-10 19:26:36 -04:00
parent f1c8958a7a
commit 3111ed5f22

View File

@@ -59,11 +59,11 @@ def test_novi_connection() -> Dict:
if not config:
return {"ok": False, "error": "Credentials not configured."}
# TODO: Confirm the actual test endpoint for the target Novi instance.
# Common pattern: GET /api/v1/members?$top=1 with ApiKey header.
# Novi uses Basic auth with a Base64-encoded API key.
# Confirmed from IDAA Jitsi integration: Authorization: Basic {api_key}
base_url = config.get('base_url', '').rstrip('/')
api_key = config.get('api_key', '')
headers = {"ApiKey": api_key, "Accept": "application/json"}
headers = {"Authorization": f"Basic {api_key}", "Accept": "application/json"}
try:
resp = requests.get(f"{base_url}/api/v1/members", headers=headers, params={"$top": 1}, timeout=10)
@@ -97,20 +97,26 @@ def get_novi_members(status_filter: Optional[str] = None, page_size: int = 500,
base_url = config.get('base_url', '').rstrip('/')
api_key = config.get('api_key', '')
headers = {"ApiKey": api_key, "Accept": "application/json"}
params = {"$top": page_size, "$skip": offset}
headers = {"Authorization": f"Basic {api_key}", "Accept": "application/json"}
params = {"pageSize": page_size, "offset": offset}
if status_filter:
params["$filter"] = f"MembershipStatus eq '{status_filter}'"
params["membershipStatus"] = status_filter
# TODO: Confirm the bulk member list endpoint for this Novi instance.
# The IDAA Jitsi code uses /customers/{uuid} for individual lookups and
# /groups/{guid}/members for group membership. A bulk member list may be
# /members, /customers, or require a group-based approach.
try:
resp = requests.get(f"{base_url}/api/v1/members", headers=headers, params=params, timeout=30)
resp = requests.get(f"{base_url}/members", headers=headers, params=params, timeout=30)
if resp.status_code != 200:
log.error(f"Novi API error: {resp.status_code} - {resp.text[:200]}")
return None
data = resp.json()
# Novi typically wraps results in a 'value' key (OData convention)
return data.get('value', data) if isinstance(data, dict) else data
# Novi may return array directly, or wrap in Results/Members key
if isinstance(data, list):
return data
return data.get('Results') or data.get('Members') or data.get('value') or []
except Exception as e:
log.exception(f"Failed to fetch Novi members: {e}")
return None
@@ -300,11 +306,12 @@ def sync_novi_to_mailman(list_id: str, active_status: str = 'Active') -> Optiona
results = {"total": len(members), "subscribed": 0, "unsubscribed": 0, "error": 0, "skipped": 0}
for member in members:
# TODO: Confirm exact field names from your Novi instance
email = (member.get('Email') or member.get('email') or '').strip()
fname = member.get('FirstName') or member.get('first_name') or ''
lname = member.get('LastName') or member.get('last_name') or ''
status = member.get('MembershipStatus') or member.get('membership_status') or ''
# Field names confirmed PascalCase from Novi API (verified via IDAA Jitsi integration).
# MembershipStatus field name still needs confirmation against the bulk member endpoint.
email = (member.get('Email') or '').strip().replace(' ', '+')
fname = member.get('FirstName') or ''
lname = member.get('LastName') or ''
status = member.get('MembershipStatus') or member.get('Status') or ''
if not email:
results['skipped'] += 1
@@ -341,10 +348,10 @@ def handle_novi_webhook(payload: Dict) -> Optional[Dict]:
"""
event_type = payload.get('EventType', '')
member = payload.get('Member', {})
email = (member.get('Email') or '').strip()
email = (member.get('Email') or '').strip().replace(' ', '+')
fname = member.get('FirstName', '')
lname = member.get('LastName', '')
status = member.get('MembershipStatus', '')
status = member.get('MembershipStatus') or member.get('Status', '')
if not email:
log.warning(f"Novi webhook received with no email — skipping. Payload: {payload}")