feat: send song requests to Hermes via webhook

- Add Hermes webhook platform support on ntfy.hallsworth.ca
- Add /admin/settings form to configure webhook 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 reply via chat or POST the Suno prompt back to /api/prompt/<id>
- Add test button, update README, bump version to 0.7.0
This commit is contained in:
Troll (Hermes Agent) 2026-08-24 22:49:08 +00:00
parent d058d8db1d
commit d7ed56f34e
6 changed files with 213 additions and 15 deletions

69
app.py
View file

@ -57,6 +57,7 @@ from helpers import (
get_max_revisions, get_callback_expiry_hours,
get_hermes_api_key, set_hermes_api_key, generate_hermes_api_key, mask_api_key,
get_ntfy_config, send_ntfy,
get_hermes_webhook_config, set_hermes_webhook_config, send_hermes_webhook,
sign_prompt_callback, verify_prompt_callback, build_prompt_callback_url,
send_email, build_signature_images,
get_booth_open,
@ -905,6 +906,37 @@ def admin_request(rid):
)
@app.route('/admin/request/<int:rid>/send-to-hermes', methods=['POST'])
def send_to_hermes(rid):
"""
Operator action: push the current request data to the configured Hermes webhook.
Hermes will generate a Suno prompt and either POST it back to the callback URL
or reply in chat.
"""
redir = require_admin()
if redir:
return redir
req = get_request_by_id(rid)
if not req:
abort(404)
if req['status'] not in ('pending', 'revisions_requested'):
flash('Request must be pending or awaiting revision to send to Hermes.', 'error')
return redirect(url_for('admin_request', rid=rid))
cfg = get_hermes_webhook_config()
if not cfg['url'] or not cfg['secret']:
flash('Hermes webhook is not configured. Set URL and secret in /admin/settings.', 'error')
return redirect(url_for('admin_request', rid=rid))
callback_url = build_prompt_callback_url(rid)
ok, msg = send_hermes_webhook(rid, req, callback_url=callback_url)
if ok:
flash(f'Sent to Hermes. {msg}', 'success')
else:
flash(f'Failed to send to Hermes: {msg}', 'error')
return redirect(url_for('admin_request', rid=rid))
@app.route('/admin/request/<int:rid>/delete', methods=['POST'])
def admin_delete_request(rid):
"""Delete a single request and remove its uploaded MP3 files."""
@ -1247,6 +1279,42 @@ def admin_settings():
flash('ntfy notification settings saved.', 'success')
return redirect(url_for('admin_settings'))
elif action == 'save_hermes_webhook':
# Update Hermes webhook URL and secret from the settings form.
webhook_url = request.form.get('hermes_webhook_url', '').strip().rstrip('/')
webhook_secret = request.form.get('hermes_webhook_secret', '').strip()
set_hermes_webhook_config(webhook_url, webhook_secret)
flash('Hermes webhook settings saved.', 'success')
return redirect(url_for('admin_settings'))
elif action == 'send_test_hermes_webhook':
# Send a test request to the configured Hermes webhook.
cfg = get_hermes_webhook_config()
if not cfg['url'] or not cfg['secret']:
flash('Configure Hermes webhook URL and secret first.', 'error')
return redirect(url_for('admin_settings'))
ok, msg = send_hermes_webhook(0, {
'email': 'test@example.com',
'name': 'Test Customer',
'pronouns': 'They/Them/Their',
'hobbies': 'testing',
'notable_facts': 'none',
'style_genre': "1980's, Pop, Funk",
'vocal_gender': 'female',
'extra_requests': 'Test webhook',
'stems_interest': 0,
'revision_count': 0,
'revision_note': '',
'suno_title': '',
'suno_style': '',
'suno_lyrics': '',
})
if ok:
flash(f'Test Hermes webhook sent. {msg}', 'success')
else:
flash(f'Test Hermes webhook failed: {msg}', 'error')
return redirect(url_for('admin_settings'))
elif action == 'send_test_ntfy':
# Send a test push notification to the configured ntfy topic.
ntfy = get_ntfy_config()
@ -1289,6 +1357,7 @@ def admin_settings():
version=current_app.config['VERSION'],
current_callback_expiry_hours=get_callback_expiry_hours(),
ntfy=runtime_settings,
hermes_webhook=get_hermes_webhook_config(),
)