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:
|
if not config:
|
||||||
return {"ok": False, "error": "Credentials not configured."}
|
return {"ok": False, "error": "Credentials not configured."}
|
||||||
|
|
||||||
# TODO: Confirm the actual test endpoint for the target Novi instance.
|
# Novi uses Basic auth with a Base64-encoded API key.
|
||||||
# Common pattern: GET /api/v1/members?$top=1 with ApiKey header.
|
# Confirmed from IDAA Jitsi integration: Authorization: Basic {api_key}
|
||||||
base_url = config.get('base_url', '').rstrip('/')
|
base_url = config.get('base_url', '').rstrip('/')
|
||||||
api_key = config.get('api_key', '')
|
api_key = config.get('api_key', '')
|
||||||
headers = {"ApiKey": api_key, "Accept": "application/json"}
|
headers = {"Authorization": f"Basic {api_key}", "Accept": "application/json"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(f"{base_url}/api/v1/members", headers=headers, params={"$top": 1}, timeout=10)
|
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('/')
|
base_url = config.get('base_url', '').rstrip('/')
|
||||||
api_key = config.get('api_key', '')
|
api_key = config.get('api_key', '')
|
||||||
headers = {"ApiKey": api_key, "Accept": "application/json"}
|
headers = {"Authorization": f"Basic {api_key}", "Accept": "application/json"}
|
||||||
params = {"$top": page_size, "$skip": offset}
|
params = {"pageSize": page_size, "offset": offset}
|
||||||
|
|
||||||
if status_filter:
|
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:
|
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:
|
if resp.status_code != 200:
|
||||||
log.error(f"Novi API error: {resp.status_code} - {resp.text[:200]}")
|
log.error(f"Novi API error: {resp.status_code} - {resp.text[:200]}")
|
||||||
return None
|
return None
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
# Novi typically wraps results in a 'value' key (OData convention)
|
# Novi may return array directly, or wrap in Results/Members key
|
||||||
return data.get('value', data) if isinstance(data, dict) else data
|
if isinstance(data, list):
|
||||||
|
return data
|
||||||
|
return data.get('Results') or data.get('Members') or data.get('value') or []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Failed to fetch Novi members: {e}")
|
log.exception(f"Failed to fetch Novi members: {e}")
|
||||||
return None
|
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}
|
results = {"total": len(members), "subscribed": 0, "unsubscribed": 0, "error": 0, "skipped": 0}
|
||||||
|
|
||||||
for member in members:
|
for member in members:
|
||||||
# TODO: Confirm exact field names from your Novi instance
|
# Field names confirmed PascalCase from Novi API (verified via IDAA Jitsi integration).
|
||||||
email = (member.get('Email') or member.get('email') or '').strip()
|
# MembershipStatus field name still needs confirmation against the bulk member endpoint.
|
||||||
fname = member.get('FirstName') or member.get('first_name') or ''
|
email = (member.get('Email') or '').strip().replace(' ', '+')
|
||||||
lname = member.get('LastName') or member.get('last_name') or ''
|
fname = member.get('FirstName') or ''
|
||||||
status = member.get('MembershipStatus') or member.get('membership_status') or ''
|
lname = member.get('LastName') or ''
|
||||||
|
status = member.get('MembershipStatus') or member.get('Status') or ''
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
results['skipped'] += 1
|
results['skipped'] += 1
|
||||||
@@ -341,10 +348,10 @@ def handle_novi_webhook(payload: Dict) -> Optional[Dict]:
|
|||||||
"""
|
"""
|
||||||
event_type = payload.get('EventType', '')
|
event_type = payload.get('EventType', '')
|
||||||
member = payload.get('Member', {})
|
member = payload.get('Member', {})
|
||||||
email = (member.get('Email') or '').strip()
|
email = (member.get('Email') or '').strip().replace(' ', '+')
|
||||||
fname = member.get('FirstName', '')
|
fname = member.get('FirstName', '')
|
||||||
lname = member.get('LastName', '')
|
lname = member.get('LastName', '')
|
||||||
status = member.get('MembershipStatus', '')
|
status = member.get('MembershipStatus') or member.get('Status', '')
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
log.warning(f"Novi webhook received with no email — skipping. Payload: {payload}")
|
log.warning(f"Novi webhook received with no email — skipping. Payload: {payload}")
|
||||||
|
|||||||
Reference in New Issue
Block a user