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:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user