Revert "feat: send song requests to Hermes via webhook"
This reverts commit d7ed56f34e.
This commit is contained in:
parent
d7ed56f34e
commit
6919ac816e
6 changed files with 15 additions and 213 deletions
33
README.md
33
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# Theme Song Booth
|
||||
|
||||
**Version:** `v0.7.0`
|
||||
**Version:** `v0.6.3`
|
||||
|
||||
A Flask web application for running a convention booth where visitors request a custom AI-generated theme song. Operators manage the queue from an admin dashboard, generate Suno prompts, upload MP3 previews, collect payment, and deliver final songs by email.
|
||||
|
||||
|
|
@ -25,13 +25,12 @@ A Flask web application for running a convention booth where visitors request a
|
|||
## What the booth does
|
||||
|
||||
1. A visitor fills out a short form at `/request`.
|
||||
2. The operator reviews the request in the admin dashboard and clicks **Send to Hermes** to push the customer data to a Hermes webhook.
|
||||
3. Hermes generates a Suno Custom Mode prompt and can POST it directly back to the `/api/prompt` callback endpoint, or reply in chat with the prompt text.
|
||||
4. The operator (or the Hermes callback) saves the prompt to the request.
|
||||
5. The operator creates two song versions in Suno, downloads them, and uploads **Version A** and **Version B** to the request page.
|
||||
6. The operator sends a preview email; the customer visits their private player page, listens to both versions, and either approves one/both or requests changes.
|
||||
7. After the customer approves and pays, the operator records the payment reference and delivers the selected MP3(s) by email.
|
||||
8. Optional stems / extras can be delivered via a share link that appears on the player page after delivery.
|
||||
2. The operator reviews the request in the admin dashboard and generates a Suno Custom Mode prompt.
|
||||
3. The operator (or an AI assistant via the `/api/prompt` callback) saves the prompt to the request.
|
||||
4. The operator creates two song versions in Suno, downloads them, and uploads **Version A** and **Version B** to the request page.
|
||||
5. The operator sends a preview email; the customer visits their private player page, listens to both versions, and either approves one/both or requests changes.
|
||||
6. After the customer approves and pays, the operator records the payment reference and delivers the selected MP3(s) by email.
|
||||
7. Optional stems / extras can be delivered via a share link that appears on the player page after delivery.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -75,10 +74,10 @@ The selected pronouns are stored in the `pronouns` column and included in confir
|
|||
|------|------|---------|
|
||||
| Login | `/admin/login` | Simple session-based login. Password comes from the `ADMIN_PASSWORD` environment variable. |
|
||||
| Dashboard | `/admin` | Main queue. Filter by status and auto-refresh at a configurable interval. |
|
||||
| Request detail | `/admin/request/<id>` | Full control of one request: edit customer info (including pronouns and structured style), save prompt, copy Hermes callback, **Send to Hermes** webhook, view revision history, upload MP3s, send preview, record payment, deliver files, add operator notes, and cancel. |
|
||||
| Request detail | `/admin/request/<id>` | Full control of one request: edit customer info (including pronouns and structured style), save prompt, copy Hermes callback, view revision history, upload MP3s, send preview, record payment, deliver files, add operator notes, and cancel. |
|
||||
| Pricing | `/admin/pricing` | Configure fixed prices (one song, both songs, WAV per song, STEMs per song) and up to 5 custom items. |
|
||||
| Sales | `/admin/sales` | Report of all delivered requests with customer details and Square payment references. |
|
||||
| Settings | `/admin/settings` | Database health, backups, SMTP config, MP3 metadata defaults, revision limit, auto-refresh interval, kiosk mode, booth open/closed switch, Hermes API key, Hermes webhook configuration, and system reset. |
|
||||
| Settings | `/admin/settings` | Database health, backups, SMTP config, MP3 metadata defaults, revision limit, auto-refresh interval, kiosk mode, booth open/closed switch, Hermes API key management, and system reset. |
|
||||
| Reset | `/admin/reset` | Clears all requests and uploaded files. Requires admin password confirmation. |
|
||||
|
||||
---
|
||||
|
|
@ -111,14 +110,9 @@ The `/admin/settings` page is split into functional sections:
|
|||
|
||||
### Hermes API key
|
||||
- Displays whether a key is configured.
|
||||
- **Regenerate API Key** creates a new random key stored in runtime settings.
|
||||
- The key is used by the `/api/prompt/<id>` callback and by the `/api/key-test` diagnostic endpoint.
|
||||
- Copy this key into your Hermes webhook subscription and AI assistant config.
|
||||
|
||||
### Hermes webhook
|
||||
- Configure the public webhook URL and HMAC secret so the booth can push song request data to Hermes.
|
||||
- The operator clicks **Send to Hermes** on a request to trigger prompt generation.
|
||||
- The payload includes the callback URL so Hermes can POST the generated prompt back to `/api/prompt/<id>`.
|
||||
- **Send Test Request** verifies the URL, secret, and signature.
|
||||
- Copy this key into your Hermes skill or AI assistant config.
|
||||
|
||||
### Customer revision limit
|
||||
- Maximum number of times a customer can click **Request Changes** on the player page.
|
||||
|
|
@ -180,8 +174,9 @@ python init_db.py
|
|||
```
|
||||
|
||||
7. Point your reverse proxy at the host port you chose (default `127.0.0.1:8000`).
|
||||
8. Visit `/admin/settings` and configure the **Hermes webhook** URL and secret.
|
||||
9. Print or display a QR code pointing to `https://your-domain/request`.
|
||||
8. Visit `/admin/settings` and click **Regenerate API Key**.
|
||||
9. Copy the key to your Hermes skill / AI assistant.
|
||||
10. Print or display a QR code pointing to `https://your-domain/request`.
|
||||
|
||||
### Updating the deployment
|
||||
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.7.0
|
||||
0.6.3
|
||||
|
|
|
|||
69
app.py
69
app.py
|
|
@ -57,7 +57,6 @@ 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,
|
||||
|
|
@ -906,37 +905,6 @@ 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."""
|
||||
|
|
@ -1279,42 +1247,6 @@ 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()
|
||||
|
|
@ -1357,7 +1289,6 @@ 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(),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
90
helpers.py
90
helpers.py
|
|
@ -374,96 +374,6 @@ 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 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()
|
||||
|
|
|
|||
|
|
@ -326,9 +326,6 @@
|
|||
<div class="section">
|
||||
<h2>1. Generate & Save Suno Prompt</h2>
|
||||
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
|
||||
<form method="POST" action="{{ url_for('send_to_hermes', rid=req.id) }}" style="display:inline">
|
||||
<button type="submit" onclick="return confirm('Send this request to Hermes to generate a Suno prompt?')">🚀 Send to Hermes</button>
|
||||
</form>
|
||||
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.</p>
|
||||
|
||||
<form method="POST">
|
||||
|
|
|
|||
|
|
@ -339,37 +339,6 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Hermes webhook integration -->
|
||||
<div class="section">
|
||||
<h2>Hermes Webhook</h2>
|
||||
<p class="copy-hint">Push song request data to Hermes so it can generate Suno prompts automatically. The URL must include the route name (e.g. https://ntfy.hallsworth.ca/webhooks/trollgorithm-song-info). The secret is encrypted before storage.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_hermes_webhook">
|
||||
<table class="meta-table">
|
||||
<tr>
|
||||
<td>Webhook URL</td>
|
||||
<td><input type="text" id="hermes_webhook_url" name="hermes_webhook_url" value="{{ hermes_webhook.get('url', '') }}" placeholder="https://ntfy.hallsworth.ca/webhooks/trollgorithm-song-info"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HMAC Secret</td>
|
||||
<td>
|
||||
<input type="password" id="hermes_webhook_secret" name="hermes_webhook_secret" placeholder="{% if hermes_webhook.get('secret') %}Stored encrypted — type to replace{% else %}Enter secret{% endif %}">
|
||||
<p class="copy-hint">From the Hermes webhook subscription. Leave blank to keep the existing stored secret.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="submit">Save Hermes Webhook</button>
|
||||
</form>
|
||||
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
|
||||
|
||||
<h3>Send Test Request</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="send_test_hermes_webhook">
|
||||
<button type="submit">Send Test to Hermes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Email / SMTP configuration -->
|
||||
<div class="section">
|
||||
<h2>Email (SMTP) Settings</h2>
|
||||
|
|
|
|||
Reference in a new issue