feat: send song requests to Hermes via webhook
- Add Hermes webhook settings form in /admin/settings (URL + HMAC secret) - Add /admin/request/<id>/send-to-hermes action - POST request data to Hermes with V2 HMAC signature and callback URL - Hermes can POST the generated MusicGPT prompt back to /api/prompt/<id> - Add test button, update README workflow, bump version to 0.9.0
This commit is contained in:
parent
3c99378044
commit
2f5050b29f
6 changed files with 199 additions and 4 deletions
91
helpers.py
91
helpers.py
|
|
@ -409,6 +409,97 @@ def get_ntfy_config():
|
|||
}
|
||||
|
||||
|
||||
def get_hermes_webhook_config():
|
||||
"""Return the Hermes webhook URL and HMAC secret from runtime settings."""
|
||||
cfg = load_booth_settings()
|
||||
return {
|
||||
'url': cfg.get('hermes_webhook_url', '').strip(),
|
||||
'secret': decrypt_value(cfg.get('hermes_webhook_secret', '')) or '',
|
||||
}
|
||||
|
||||
|
||||
def set_hermes_webhook_config(url, secret):
|
||||
"""Persist Hermes webhook URL and secret (encrypted) to runtime settings."""
|
||||
cfg = load_booth_settings()
|
||||
cfg['hermes_webhook_url'] = url.strip().rstrip('/')
|
||||
if secret:
|
||||
cfg['hermes_webhook_secret'] = encrypt_value(secret)
|
||||
save_booth_settings(cfg)
|
||||
|
||||
|
||||
def send_hermes_webhook(rid, req, callback_url=None):
|
||||
"""
|
||||
POST a song request payload to the configured Hermes webhook.
|
||||
|
||||
Returns (success: bool, message: str). On success, Hermes receives the
|
||||
customer data and can generate a Suno/MusicGPT prompt. If callback_url is
|
||||
provided, Hermes can POST the generated prompt directly back to
|
||||
/api/prompt/<rid>.
|
||||
"""
|
||||
cfg = get_hermes_webhook_config()
|
||||
url = cfg.get('url', '')
|
||||
secret = cfg.get('secret', '')
|
||||
if not url or not secret:
|
||||
return False, 'Hermes webhook is not configured in /admin/settings'
|
||||
|
||||
style_genre = req.get('style_genre') or ''
|
||||
style_parts = [p.strip() for p in style_genre.split(',') if p.strip()]
|
||||
style_sentence = ''
|
||||
if style_parts:
|
||||
parts = []
|
||||
if style_parts[0]:
|
||||
parts.append(f"{style_parts[0]}-era")
|
||||
if len(style_parts) > 1 and style_parts[1]:
|
||||
parts.append(style_parts[1])
|
||||
if len(style_parts) > 2:
|
||||
parts.append(f"with {', '.join(style_parts[2:])} influences")
|
||||
style_sentence = ' '.join(parts)
|
||||
|
||||
def _bool(value):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return bool(int(value or 0))
|
||||
|
||||
payload = {
|
||||
'event_type': 'song_request',
|
||||
'request_id': rid,
|
||||
'email': req.get('email', ''),
|
||||
'name': req.get('name', ''),
|
||||
'pronouns': req.get('pronouns', ''),
|
||||
'hobbies': req.get('hobbies', ''),
|
||||
'notable_facts': req.get('notable_facts', ''),
|
||||
'style_genre': style_sentence or style_genre,
|
||||
'vocal_gender': req.get('vocal_gender', ''),
|
||||
'extra_requests': req.get('extra_requests', ''),
|
||||
'stems_interest': _bool(req.get('stems_interest')),
|
||||
'is_revision': bool(req.get('revision_count', 0)) and bool(req.get('revision_note')),
|
||||
'revision_count': int(req.get('revision_count') or 0),
|
||||
'revision_note': req.get('revision_note', ''),
|
||||
'previous_title': req.get('suno_title', ''),
|
||||
'previous_style': req.get('suno_style', ''),
|
||||
'previous_lyrics': req.get('suno_lyrics', ''),
|
||||
'callback_url': callback_url or '',
|
||||
}
|
||||
|
||||
body = json.dumps(payload, separators=(',', ':')).encode('utf-8')
|
||||
timestamp = str(int(time.time()))
|
||||
sig_data = f"{timestamp}.{body.decode('utf-8')}"
|
||||
signature = hmac.new(secret.encode('utf-8'), sig_data.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Webhook-Signature-V2': signature,
|
||||
'X-Webhook-Timestamp': timestamp,
|
||||
}
|
||||
try:
|
||||
resp = requests.post(url, data=body, headers=headers, timeout=15)
|
||||
if resp.status_code in (200, 202):
|
||||
return True, f"Sent to Hermes (HTTP {resp.status_code})"
|
||||
return False, f"Hermes webhook returned HTTP {resp.status_code}: {resp.text[:200]}"
|
||||
except Exception as e:
|
||||
return False, f"Failed to reach Hermes webhook: {e}"
|
||||
|
||||
|
||||
def send_ntfy(message, title='Theme Song Booth', priority='default', tags='bell'):
|
||||
"""Send a push notification to the configured ntfy topic, if configured."""
|
||||
ntfy = get_ntfy_config()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue