Ficha privada de invitados y condiciones de participación
- /invitados.html (noindex) con enlace personal ?k=<código>; las respuestas llegan por correo desde contacto@ en lugar de Google Forms. - server.py: /api/invitados/check y /api/invitados (códigos en INVITE_TOKENS). - terminos.html: sección 5 con las condiciones de participación. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSyR5Zw5eGj6oPTRVjvP1b
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
f0682d7de8
commit
da8cc01fad
@@ -3,6 +3,7 @@ RUN apk add --no-cache python3 py3-pillow
|
|||||||
COPY public/ /usr/share/nginx/html/
|
COPY public/ /usr/share/nginx/html/
|
||||||
COPY build.py /app/build.py
|
COPY build.py /app/build.py
|
||||||
COPY server.py /app/server.py
|
COPY server.py /app/server.py
|
||||||
|
COPY guest_form.json /app/guest_form.json
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
{
|
||||||
|
"title": "Anda Que No Sabes Ná Podcast",
|
||||||
|
"description": "Queremos conocerte un poco antes de darle al REC. No hace falta que prepares respuestas perfectas: esto nos sirve para encontrar buenas historias, preguntas interesantes y, sobre todo, temas sobre los que merezca la pena hablar.",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": 11,
|
||||||
|
"title": "",
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "👤 Tú",
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"title": "Nombre y cómo prefieres que te presentemos",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿A qué te dedicas actualmente?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Cuál es tu proyecto, empresa o actividad principal?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"title": "LinkedIn, web o cualquier enlace que nos ayude a conocerte mejor",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Tus redes sociales que quieras que mencionemos o utilicemos para promocionar el episodio",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "🎙️ Para conocerte un poco mejor",
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Si tuvieras que contarnos tu historia en dos minutos, ¿por dónde empezarías?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Qué tema podrías estar hablando durante horas sin aburrirte?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna experiencia, proyecto, fracaso, éxito o anécdota que te parezca especialmente buena para contar en el podcast?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Qué idea, mito o tópico de tu sector te gustaría desmontar?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "🔥 Vamos a meternos en harina",
|
||||||
|
"desc": "Contestar en esta sección es opcional"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna opinión tuya sobre tu sector que pueda generar debate?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Qué pregunta te hacen siempre y estás cansado/a de responder?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay algo que normalmente no se cuenta cuando se habla de tu profesión, sector o trayectoria y que crees que deberíamos contar?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Si pudiéramos conseguir que después de escucharte alguien cambiara de opinión sobre una cosa, ¿qué te gustaría que fuera?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "🚧 Territorio delicado",
|
||||||
|
"desc": "No hace falta justificarlo. Queremos saber dónde están tus límites antes de empezar."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay algún tema que prefieras que no toquemos?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna persona, empresa, experiencia o asunto sobre el que prefieras que no hagamos preguntas?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Y... ¿Hay algún otro tema de tu vida que quieras contarnos?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "📣 Y para terminar...",
|
||||||
|
"desc": "Contestar en esta sección es opcional"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay algún proyecto, libro, producto, evento o iniciativa que tengas ahora mismo y que te gustaría dar a conocer durante la entrevista?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna pregunta que te encantaría que te hiciéramos y que normalmente nadie te hace?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna historia que te gustaría contar sí o sí?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "Antes de enviar",
|
||||||
|
"desc": "Gracias por tu tiempo y tu disposición. Este podcast se realiza de forma altruista y colaborativa. Ni la preparación, ni la grabación, ni la publicación contemplan remuneración económica. La participación es voluntaria y con buena fe, con la intención de compartir conocimiento, experiencias e historias. Al aceptar participar, entiendes y estás de acuerdo con estas condiciones."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 4,
|
||||||
|
"title": "Aceptación de condiciones",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
"He leído y acepto las condiciones de participación descritas arriba."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""Genera public/invitados.html a partir de guest_form.json (copia de las preguntas del formulario de Google)."""
|
||||||
|
import html, json
|
||||||
|
|
||||||
|
esc = html.escape
|
||||||
|
F = json.load(open('guest_form.json', encoding='utf-8'))
|
||||||
|
|
||||||
|
secciones, actual, nq = [], None, 0
|
||||||
|
consent = None
|
||||||
|
for it in F['items']:
|
||||||
|
t = it['type']
|
||||||
|
if t == 8:
|
||||||
|
actual = {'titulo': it['title'], 'nota': it['desc'], 'preguntas': []}
|
||||||
|
secciones.append(actual)
|
||||||
|
elif t in (0, 1):
|
||||||
|
nq += 1
|
||||||
|
actual['preguntas'].append({'id': f'q{nq:02d}', 'titulo': it['title'], 'desc': it['desc'],
|
||||||
|
'req': it['required'], 'largo': t == 1})
|
||||||
|
elif t == 4:
|
||||||
|
consent = {'titulo': it['title'], 'opcion': it['options'][0], 'req': it['required']}
|
||||||
|
consent_text = secciones[-1]['nota']
|
||||||
|
secciones.pop() # la última sección solo contiene las condiciones
|
||||||
|
|
||||||
|
def pregunta(q):
|
||||||
|
mark = ' <span class="req" title="Obligatoria">*</span>' if q['req'] else ''
|
||||||
|
campo = (f'<textarea name="{q["id"]}" rows="4" maxlength="3000"{" required" if q["req"] else ""}></textarea>'
|
||||||
|
if q['largo'] else
|
||||||
|
f'<input name="{q["id"]}" type="text" maxlength="300"{" required" if q["req"] else ""}>')
|
||||||
|
d = f'<small>{esc(q["desc"])}</small>' if q['desc'] else ''
|
||||||
|
return f' <label>{esc(q["titulo"])}{mark}{d}\n {campo}\n </label>'
|
||||||
|
|
||||||
|
bloques = []
|
||||||
|
for s in secciones:
|
||||||
|
nota = f'\n <p class="note">{esc(s["nota"])}</p>' if s['nota'] else ''
|
||||||
|
bloques.append(f' <fieldset class="sec">\n <legend>{esc(s["titulo"])}</legend>{nota}\n' +
|
||||||
|
'\n'.join(pregunta(q) for q in s['preguntas']) + '\n </fieldset>')
|
||||||
|
|
||||||
|
page = f'''<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
|
<title>Ficha del invitado · Anda Que No Sabes Ná</title>
|
||||||
|
<meta name="theme-color" content="#06080d">
|
||||||
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="legal contact guest">
|
||||||
|
<h1>Ficha del <em>invitado</em></h1>
|
||||||
|
<p id="hola" class="lead-c" hidden></p>
|
||||||
|
<p id="malo" class="status err" hidden>Este enlace no es válido o ha caducado. Escríbenos a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a> y te mandamos uno nuevo.</p>
|
||||||
|
<p id="intro" class="lead-c" hidden>{esc(F['description'])}</p>
|
||||||
|
|
||||||
|
<form id="g" class="form" novalidate hidden data-ok="¡Gracias! Hemos recibido tu ficha. Te escribiremos pronto con los siguientes pasos.">
|
||||||
|
<label>Tu correo electrónico (opcional, para poder responderte)
|
||||||
|
<input name="email" type="email" autocomplete="email" maxlength="200">
|
||||||
|
</label>
|
||||||
|
{chr(10).join(bloques)}
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>Antes de enviar</legend>
|
||||||
|
<p class="note">Este podcast se realiza de forma altruista y colaborativa. Antes de enviar, lee las <a href="terminos.html#participacion" target="_blank" rel="noopener">condiciones de participación</a>.</p>
|
||||||
|
<div class="trap" aria-hidden="true"><label>No rellenes este campo <input name="web" type="text" tabindex="-1" autocomplete="off"></label></div>
|
||||||
|
<label class="check">
|
||||||
|
<input name="consent" type="checkbox" required>
|
||||||
|
<span>He leído y acepto las <a href="terminos.html#participacion" target="_blank" rel="noopener">condiciones de participación</a> descritas. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
<label class="check">
|
||||||
|
<input name="privacy" type="checkbox" required>
|
||||||
|
<span>He leído la <a href="privacidad.html" target="_blank" rel="noopener">política de privacidad</a> y acepto que usen estos datos para preparar el episodio. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<button class="btn primary" type="submit">Enviar mi ficha</button>
|
||||||
|
<p id="st" class="status" role="status" aria-live="polite"></p>
|
||||||
|
<p class="note">Tus respuestas se guardan en este navegador mientras escribes, por si cierras la página. Se borran al enviarlas.</p>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="invitados.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
open('public/invitados.html', 'w', encoding='utf-8').write(page)
|
||||||
|
print('invitados.html:', len(page), 'bytes;', nq, 'preguntas;', len(secciones), 'secciones')
|
||||||
+10
@@ -10,6 +10,16 @@ server {
|
|||||||
location = / { add_header Cache-Control "no-cache"; }
|
location = / { add_header Cache-Control "no-cache"; }
|
||||||
location /img/ { expires 7d; add_header Cache-Control "public"; }
|
location /img/ { expires 7d; add_header Cache-Control "public"; }
|
||||||
location ~* \.(css)$ { add_header Cache-Control "no-cache"; }
|
location ~* \.(css)$ { add_header Cache-Control "no-cache"; }
|
||||||
|
location = /invitados.html {
|
||||||
|
add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
}
|
||||||
|
location = /api/invitados {
|
||||||
|
client_max_body_size 128k;
|
||||||
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
|
||||||
|
}
|
||||||
location /api/ {
|
location /api/ {
|
||||||
client_max_body_size 16k;
|
client_max_body_size 16k;
|
||||||
proxy_pass http://127.0.0.1:8081;
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
|
<title>Ficha del invitado · Anda Que No Sabes Ná</title>
|
||||||
|
<meta name="theme-color" content="#06080d">
|
||||||
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="legal contact guest">
|
||||||
|
<h1>Ficha del <em>invitado</em></h1>
|
||||||
|
<p id="hola" class="lead-c" hidden></p>
|
||||||
|
<p id="malo" class="status err" hidden>Este enlace no es válido o ha caducado. Escríbenos a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a> y te mandamos uno nuevo.</p>
|
||||||
|
<p id="intro" class="lead-c" hidden>Queremos conocerte un poco antes de darle al REC. No hace falta que prepares respuestas perfectas: esto nos sirve para encontrar buenas historias, preguntas interesantes y, sobre todo, temas sobre los que merezca la pena hablar.</p>
|
||||||
|
|
||||||
|
<form id="g" class="form" novalidate hidden data-ok="¡Gracias! Hemos recibido tu ficha. Te escribiremos pronto con los siguientes pasos.">
|
||||||
|
<label>Tu correo electrónico (opcional, para poder responderte)
|
||||||
|
<input name="email" type="email" autocomplete="email" maxlength="200">
|
||||||
|
</label>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>👤 Tú</legend>
|
||||||
|
<label>Nombre y cómo prefieres que te presentemos <span class="req" title="Obligatoria">*</span>
|
||||||
|
<input name="q01" type="text" maxlength="300" required>
|
||||||
|
</label>
|
||||||
|
<label>¿A qué te dedicas actualmente? <span class="req" title="Obligatoria">*</span>
|
||||||
|
<textarea name="q02" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Cuál es tu proyecto, empresa o actividad principal?
|
||||||
|
<textarea name="q03" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>LinkedIn, web o cualquier enlace que nos ayude a conocerte mejor
|
||||||
|
<input name="q04" type="text" maxlength="300">
|
||||||
|
</label>
|
||||||
|
<label>Tus redes sociales que quieras que mencionemos o utilicemos para promocionar el episodio
|
||||||
|
<textarea name="q05" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>🎙️ Para conocerte un poco mejor</legend>
|
||||||
|
<label>Si tuvieras que contarnos tu historia en dos minutos, ¿por dónde empezarías? <span class="req" title="Obligatoria">*</span>
|
||||||
|
<textarea name="q06" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Qué tema podrías estar hablando durante horas sin aburrirte? <span class="req" title="Obligatoria">*</span>
|
||||||
|
<textarea name="q07" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Hay alguna experiencia, proyecto, fracaso, éxito o anécdota que te parezca especialmente buena para contar en el podcast? <span class="req" title="Obligatoria">*</span>
|
||||||
|
<textarea name="q08" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Qué idea, mito o tópico de tu sector te gustaría desmontar?
|
||||||
|
<textarea name="q09" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>🔥 Vamos a meternos en harina</legend>
|
||||||
|
<p class="note">Contestar en esta sección es opcional</p>
|
||||||
|
<label>¿Hay alguna opinión tuya sobre tu sector que pueda generar debate?
|
||||||
|
<textarea name="q10" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Qué pregunta te hacen siempre y estás cansado/a de responder?
|
||||||
|
<textarea name="q11" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Hay algo que normalmente no se cuenta cuando se habla de tu profesión, sector o trayectoria y que crees que deberíamos contar?
|
||||||
|
<textarea name="q12" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>Si pudiéramos conseguir que después de escucharte alguien cambiara de opinión sobre una cosa, ¿qué te gustaría que fuera?
|
||||||
|
<textarea name="q13" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>🚧 Territorio delicado</legend>
|
||||||
|
<p class="note">No hace falta justificarlo. Queremos saber dónde están tus límites antes de empezar.</p>
|
||||||
|
<label>¿Hay algún tema que prefieras que no toquemos? <span class="req" title="Obligatoria">*</span>
|
||||||
|
<textarea name="q14" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Hay alguna persona, empresa, experiencia o asunto sobre el que prefieras que no hagamos preguntas?
|
||||||
|
<textarea name="q15" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>Y... ¿Hay algún otro tema de tu vida que quieras contarnos?
|
||||||
|
<textarea name="q16" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>📣 Y para terminar...</legend>
|
||||||
|
<p class="note">Contestar en esta sección es opcional</p>
|
||||||
|
<label>¿Hay algún proyecto, libro, producto, evento o iniciativa que tengas ahora mismo y que te gustaría dar a conocer durante la entrevista?
|
||||||
|
<textarea name="q17" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Hay alguna pregunta que te encantaría que te hiciéramos y que normalmente nadie te hace?
|
||||||
|
<textarea name="q18" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label>¿Hay alguna historia que te gustaría contar sí o sí?
|
||||||
|
<textarea name="q19" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>Antes de enviar</legend>
|
||||||
|
<p class="note">Este podcast se realiza de forma altruista y colaborativa. Antes de enviar, lee las <a href="terminos.html#participacion" target="_blank" rel="noopener">condiciones de participación</a>.</p>
|
||||||
|
<div class="trap" aria-hidden="true"><label>No rellenes este campo <input name="web" type="text" tabindex="-1" autocomplete="off"></label></div>
|
||||||
|
<label class="check">
|
||||||
|
<input name="consent" type="checkbox" required>
|
||||||
|
<span>He leído y acepto las <a href="terminos.html#participacion" target="_blank" rel="noopener">condiciones de participación</a> descritas. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
<label class="check">
|
||||||
|
<input name="privacy" type="checkbox" required>
|
||||||
|
<span>He leído la <a href="privacidad.html" target="_blank" rel="noopener">política de privacidad</a> y acepto que usen estos datos para preparar el episodio. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<button class="btn primary" type="submit">Enviar mi ficha</button>
|
||||||
|
<p id="st" class="status" role="status" aria-live="polite"></p>
|
||||||
|
<p class="note">Tus respuestas se guardan en este navegador mientras escribes, por si cierras la página. Se borran al enviarlas.</p>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="invitados.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
(function () {
|
||||||
|
var $ = function (id) { return document.getElementById(id); };
|
||||||
|
var k = (new URLSearchParams(location.search).get('k') || '').trim();
|
||||||
|
var f = $('g'), st = $('st'), hola = $('hola'), malo = $('malo'), intro = $('intro');
|
||||||
|
var t0 = Date.now(), busy = false, KEY = 'aqnsn-ficha-' + k.slice(0, 10);
|
||||||
|
function say(m, ok) { st.textContent = m; st.className = 'status ' + (ok ? 'ok' : 'err'); }
|
||||||
|
function store(op, v) { try { return op === 'get' ? localStorage.getItem(KEY) : op === 'set' ? localStorage.setItem(KEY, v) : localStorage.removeItem(KEY); } catch (e) { return null; } }
|
||||||
|
if (!k) { malo.hidden = false; return; }
|
||||||
|
|
||||||
|
fetch('/api/invitados/check?k=' + encodeURIComponent(k), { cache: 'no-store' })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (j) {
|
||||||
|
if (!j.ok) { malo.hidden = false; return; }
|
||||||
|
hola.textContent = 'Hola, ' + j.nombre + '. ¡Gracias por querer venir al programa!';
|
||||||
|
hola.hidden = false; intro.hidden = false; f.hidden = false;
|
||||||
|
try { var d = JSON.parse(store('get') || '{}'); Object.keys(d).forEach(function (n) { if (f.elements[n] && f.elements[n].type !== 'checkbox') f.elements[n].value = d[n]; }); } catch (e) {}
|
||||||
|
})
|
||||||
|
.catch(function () { malo.hidden = false; });
|
||||||
|
|
||||||
|
var timer;
|
||||||
|
f.addEventListener('input', function () {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(function () {
|
||||||
|
var d = {};
|
||||||
|
Array.prototype.forEach.call(f.elements, function (el) { if (el.name && el.type !== 'checkbox' && el.name !== 'web' && el.value) d[el.name] = el.value; });
|
||||||
|
store('set', JSON.stringify(d));
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
f.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (busy) return;
|
||||||
|
var resp = {}, falta = null;
|
||||||
|
Array.prototype.forEach.call(f.elements, function (el) {
|
||||||
|
if (/^q\d+$/.test(el.name)) { resp[el.name] = el.value.trim(); if (el.required && !resp[el.name] && !falta) falta = el; }
|
||||||
|
});
|
||||||
|
if (falta) { falta.focus(); say('Faltan respuestas obligatorias (las marcadas con *).', false); return; }
|
||||||
|
if (!f.consent.checked || !f.privacy.checked) { say('Marca las dos casillas finales para poder enviar la ficha.', false); return; }
|
||||||
|
var email = (f.email.value || '').trim();
|
||||||
|
if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(email)) { say('El correo no parece válido.', false); return; }
|
||||||
|
busy = true; say('Enviando…', true);
|
||||||
|
fetch('/api/invitados', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ k: k, email: email, resp: resp, consent: true, web: f.web.value || '', t: Date.now() - t0 }) })
|
||||||
|
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
|
||||||
|
.then(function (x) {
|
||||||
|
if (x.ok) { store('del'); f.reset(); Array.prototype.forEach.call(f.querySelectorAll('fieldset,label,button,.note'), function (n) { n.hidden = true; }); say(f.getAttribute('data-ok'), true); }
|
||||||
|
else say((x.j && x.j.error) || 'No se pudo enviar. Escríbenos a andaquenosabesnada@gmail.com.', false);
|
||||||
|
})
|
||||||
|
.catch(function () { say('No se pudo enviar. Escríbenos a andaquenosabesnada@gmail.com.', false); })
|
||||||
|
.then(function () { busy = false; });
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -28,10 +28,12 @@
|
|||||||
<p>Como cualquier servidor web, nuestro servidor puede registrar de forma temporal la dirección IP, la fecha y la página solicitada, únicamente por seguridad y para detectar abusos. No usamos esos registros para identificar a nadie ni los compartimos.</p>
|
<p>Como cualquier servidor web, nuestro servidor puede registrar de forma temporal la dirección IP, la fecha y la página solicitada, únicamente por seguridad y para detectar abusos. No usamos esos registros para identificar a nadie ni los compartimos.</p>
|
||||||
<p>Los enlaces a YouTube, TikTok e Instagram llevan a servicios de terceros, que tienen sus propias políticas de privacidad.</p>
|
<p>Los enlaces a YouTube, TikTok e Instagram llevan a servicios de terceros, que tienen sus propias políticas de privacidad.</p>
|
||||||
|
|
||||||
<h2>3. Formularios de contacto y de patrocinio</h2>
|
<h2>3. Formularios de contacto, patrocinio y ficha de invitados</h2>
|
||||||
<p>Si nos escribes desde la página de <a href="contacto.html">contacto</a> o de <a href="patrocinadores.html">patrocinadores</a>, recogemos los datos que tú mismo introduces: nombre, correo electrónico, mensaje y motivo o tipo de colaboración y, en el formulario de patrocinio, también el nombre de la empresa y su web si la indicas. Los usamos <strong>únicamente para responderte</strong>. La base legal es tu consentimiento, que das al marcar la casilla del formulario.</p>
|
<p>Si nos escribes desde la página de <a href="contacto.html">contacto</a> o de <a href="patrocinadores.html">patrocinadores</a>, recogemos los datos que tú mismo introduces: nombre, correo electrónico, mensaje y motivo o tipo de colaboración y, en el formulario de patrocinio, también el nombre de la empresa y su web si la indicas. Los usamos <strong>únicamente para responderte</strong>. La base legal es tu consentimiento, que das al marcar la casilla del formulario.</p>
|
||||||
<p>La web no guarda los mensajes: el formulario los reenvía por correo electrónico, a través de nuestro propio servidor de correo, a la cuenta del programa. Los conservamos solo el tiempo necesario para atender tu consulta y no los cedemos a terceros ni los usamos para otra cosa. Los formularios incluyen medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</p>
|
<p>La web no guarda los mensajes: el formulario los reenvía por correo electrónico, a través de nuestro propio servidor de correo, a la cuenta del programa. Los conservamos solo el tiempo necesario para atender tu consulta y no los cedemos a terceros ni los usamos para otra cosa. Los formularios incluyen medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</p>
|
||||||
|
|
||||||
|
<p><strong>Ficha del invitado.</strong> Si participas como invitado y rellenas la ficha (a la que se accede solo mediante un enlace personal), recogemos las respuestas que escribes, que pueden incluir datos sobre tu actividad profesional, tu trayectoria y, si tú decides contarlo, aspectos personales. Las usamos únicamente para preparar la entrevista y el episodio, y se envían por correo electrónico al equipo del programa; la web no las guarda. No las cedemos a terceros. La base legal es tu consentimiento, que das al aceptar las <a href="terminos.html#participacion">condiciones de participación</a> y esta política, y puedes pedirnos que las borremos en cualquier momento.</p>
|
||||||
|
|
||||||
<h2>4. Nuestra aplicación de publicación</h2>
|
<h2>4. Nuestra aplicación de publicación</h2>
|
||||||
<p>Usamos una aplicación de gestión de redes sociales de código abierto (Postiz), instalada en un servidor propio, para programar y publicar el contenido del programa. La aplicación está pensada para uso interno: <strong>solo la utiliza el propietario de las cuentas</strong> y no está abierta al público.</p>
|
<p>Usamos una aplicación de gestión de redes sociales de código abierto (Postiz), instalada en un servidor propio, para programar y publicar el contenido del programa. La aplicación está pensada para uso interno: <strong>solo la utiliza el propietario de las cuentas</strong> y no está abierta al público.</p>
|
||||||
<p>Para funcionar, la aplicación solicita autorización a las cuentas del propio programa y accede a:</p>
|
<p>Para funcionar, la aplicación solicita autorización a las cuentas del propio programa y accede a:</p>
|
||||||
|
|||||||
@@ -239,3 +239,12 @@ footer { margin-top: 40px; border-top: 1px solid var(--line); background: var(--
|
|||||||
.views b { font-weight: 700; }
|
.views b { font-weight: 700; }
|
||||||
.scard .views { left: 6px; bottom: 6px; padding: 2px 9px 2px 7px; font-size: .72rem; }
|
.scard .views { left: 6px; bottom: 6px; padding: 2px 9px 2px 7px; font-size: .72rem; }
|
||||||
.feat .views { left: 14px; bottom: 14px; font-size: .9rem; padding: 4px 13px 4px 11px; }
|
.feat .views { left: 14px; bottom: 14px; font-size: .9rem; padding: 4px 13px 4px 11px; }
|
||||||
|
|
||||||
|
/* ficha del invitado */
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
.guest .sec { display: grid; gap: 18px; margin: 0 0 22px; padding: 20px 22px 24px; border: 1px solid var(--line); border-radius: 16px; background: linear-gradient(135deg, var(--card), var(--bg2)); }
|
||||||
|
.guest .sec legend { padding: 0 10px; font-weight: 700; font-size: 1.1rem; color: var(--neon); }
|
||||||
|
.guest .form small { display: block; margin-top: 2px; color: var(--muted); font-weight: 400; }
|
||||||
|
.guest .note, .note { margin: 0; color: var(--muted); font-size: .92rem; }
|
||||||
|
.guest .note a { color: var(--neon); text-decoration: underline; text-underline-offset: 3px; }
|
||||||
|
.req { color: var(--amber); font-weight: 700; }
|
||||||
|
|||||||
+17
-5
@@ -36,19 +36,31 @@
|
|||||||
<li>Condiciones de uso de Instagram (Meta)</li>
|
<li>Condiciones de uso de Instagram (Meta)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>5. Propiedad intelectual</h2>
|
<h2 id="participacion">5. Condiciones de participación de invitados</h2>
|
||||||
|
<p>Estas condiciones se aplican a las personas que, tras ser invitadas, participan en el programa <strong>Anda Que No Sabes Ná</strong> y rellenan la <em>ficha del invitado</em>. Se aceptan marcando la casilla correspondiente al final de dicha ficha.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Proyecto altruista y colaborativo.</strong> El programa se realiza de forma altruista y colaborativa, con la intención de compartir conocimiento, experiencias e historias.</li>
|
||||||
|
<li><strong>Sin remuneración económica.</strong> Ni la preparación, ni la grabación, ni la publicación del episodio contemplan remuneración económica.</li>
|
||||||
|
<li><strong>Participación voluntaria y de buena fe.</strong> La participación es voluntaria y se realiza de buena fe.</li>
|
||||||
|
<li><strong>Ficha previa.</strong> La ficha sirve para conocer al invitado antes de la grabación y encontrar historias, preguntas y temas sobre los que merezca la pena hablar. No es necesario preparar respuestas perfectas, y las secciones indicadas como opcionales pueden dejarse en blanco.</li>
|
||||||
|
<li><strong>Límites temáticos.</strong> En la ficha, el invitado puede indicar los temas, personas, empresas o asuntos sobre los que prefiere que no se hable, sin necesidad de justificarlo. El equipo del programa tendrá en cuenta estas indicaciones al preparar la entrevista.</li>
|
||||||
|
<li><strong>Aceptación.</strong> Al marcar la casilla de aceptación y enviar la ficha, el invitado declara haber leído y entendido estas condiciones y estar de acuerdo con ellas.</li>
|
||||||
|
</ul>
|
||||||
|
<p>El tratamiento de los datos que se faciliten en la ficha se describe en la <a href="privacidad.html">política de privacidad</a>. Para cualquier duda sobre estas condiciones, escríbenos a <a href="mailto:contacto@andaquenosabesna.com">contacto@andaquenosabesna.com</a>.</p>
|
||||||
|
|
||||||
|
<h2>6. Propiedad intelectual</h2>
|
||||||
<p>Los vídeos, textos, imágenes, el nombre y el logotipo de Anda Que No Sabes Ná pertenecen a sus autores y a las personas que participan en el programa. No está permitido reproducirlos, distribuirlos ni usarlos con fines comerciales sin autorización previa por escrito. Puedes compartir los enlaces a nuestros vídeos libremente.</p>
|
<p>Los vídeos, textos, imágenes, el nombre y el logotipo de Anda Que No Sabes Ná pertenecen a sus autores y a las personas que participan en el programa. No está permitido reproducirlos, distribuirlos ni usarlos con fines comerciales sin autorización previa por escrito. Puedes compartir los enlaces a nuestros vídeos libremente.</p>
|
||||||
|
|
||||||
<h2>6. Enlaces externos</h2>
|
<h2>7. Enlaces externos</h2>
|
||||||
<p>La web contiene enlaces a sitios de terceros. No controlamos su contenido ni somos responsables de sus políticas o prácticas.</p>
|
<p>La web contiene enlaces a sitios de terceros. No controlamos su contenido ni somos responsables de sus políticas o prácticas.</p>
|
||||||
|
|
||||||
<h2>7. Limitación de responsabilidad</h2>
|
<h2>8. Limitación de responsabilidad</h2>
|
||||||
<p>La web se ofrece «tal cual», sin garantías de disponibilidad continua. En la medida permitida por la ley, no seremos responsables de los daños derivados de su uso o de la imposibilidad de acceder a ella. Las opiniones expresadas en el programa son de quienes las dicen.</p>
|
<p>La web se ofrece «tal cual», sin garantías de disponibilidad continua. En la medida permitida por la ley, no seremos responsables de los daños derivados de su uso o de la imposibilidad de acceder a ella. Las opiniones expresadas en el programa son de quienes las dicen.</p>
|
||||||
|
|
||||||
<h2>8. Cambios</h2>
|
<h2>9. Cambios</h2>
|
||||||
<p>Podemos actualizar estas condiciones. La versión vigente es la publicada en esta página, con su fecha de actualización.</p>
|
<p>Podemos actualizar estas condiciones. La versión vigente es la publicada en esta página, con su fecha de actualización.</p>
|
||||||
|
|
||||||
<h2>9. Ley aplicable y contacto</h2>
|
<h2>10. Ley aplicable y contacto</h2>
|
||||||
<p>Estas condiciones se rigen por la legislación española. Para cualquier duda, escríbenos a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a>.</p>
|
<p>Estas condiciones se rigen por la legislación española. Para cualquier duda, escríbenos a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a>.</p>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ No guarda los mensajes en disco: solo los reenvía por correo.
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
import re
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
import ssl
|
import ssl
|
||||||
@@ -56,7 +57,8 @@ def send(subject, reply_to, body):
|
|||||||
msg['Subject'] = subject
|
msg['Subject'] = subject
|
||||||
msg['From'] = os.environ.get('MAIL_FROM', user)
|
msg['From'] = os.environ.get('MAIL_FROM', user)
|
||||||
msg['To'] = os.environ['MAIL_TO']
|
msg['To'] = os.environ['MAIL_TO']
|
||||||
msg['Reply-To'] = reply_to
|
if reply_to:
|
||||||
|
msg['Reply-To'] = reply_to
|
||||||
msg.set_content(body)
|
msg.set_content(body)
|
||||||
s = smtplib.SMTP(host, int(os.environ.get('SMTP_PORT', '587')), timeout=20)
|
s = smtplib.SMTP(host, int(os.environ.get('SMTP_PORT', '587')), timeout=20)
|
||||||
try:
|
try:
|
||||||
@@ -73,6 +75,50 @@ def send(subject, reply_to, body):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_guest():
|
||||||
|
"""Preguntas de la ficha (copia del formulario de Google) -> [{id, sec, t, req}]."""
|
||||||
|
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'guest_form.json')
|
||||||
|
try:
|
||||||
|
form = json.load(open(p, encoding='utf-8'))
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
out, sec, n = [], '', 0
|
||||||
|
for it in form['items']:
|
||||||
|
if it['type'] == 8:
|
||||||
|
sec = it['title']
|
||||||
|
elif it['type'] in (0, 1):
|
||||||
|
n += 1
|
||||||
|
out.append({'id': f'q{n:02d}', 'sec': sec, 't': it['title'], 'req': it['required']})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
GUEST_Q = load_guest()
|
||||||
|
check_hits = defaultdict(deque)
|
||||||
|
guest_hits = defaultdict(deque)
|
||||||
|
|
||||||
|
|
||||||
|
def limited(bucket, ip, maxn, window=3600):
|
||||||
|
now = time.time()
|
||||||
|
q = bucket[ip]
|
||||||
|
while q and now - q[0] > window:
|
||||||
|
q.popleft()
|
||||||
|
if len(q) >= maxn:
|
||||||
|
return True
|
||||||
|
q.append(now)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def invites():
|
||||||
|
"""INVITE_TOKENS="codigo=Nombre;codigo2=Nombre2" (códigos de 16+ caracteres)."""
|
||||||
|
d = {}
|
||||||
|
for part in os.environ.get('INVITE_TOKENS', '').split(';'):
|
||||||
|
if '=' in part:
|
||||||
|
k, v = part.split('=', 1)
|
||||||
|
if len(k.strip()) >= 16 and v.strip():
|
||||||
|
d[k.strip()] = v.strip()
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
class H(BaseHTTPRequestHandler):
|
class H(BaseHTTPRequestHandler):
|
||||||
server_version = 'aqnsn'
|
server_version = 'aqnsn'
|
||||||
|
|
||||||
@@ -88,7 +134,63 @@ class H(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def client_ip(self):
|
||||||
|
return (self.headers.get('X-Forwarded-For') or self.client_address[0]).split(',')[0].strip()
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if urlparse(self.path).path != '/api/invitados/check':
|
||||||
|
return self.reply(404, {'ok': False})
|
||||||
|
if limited(check_hits, self.client_ip(), 40):
|
||||||
|
return self.reply(429, {'ok': False})
|
||||||
|
k = (parse_qs(urlparse(self.path).query).get('k') or [''])[0].strip()
|
||||||
|
nombre = invites().get(k)
|
||||||
|
return self.reply(200, {'ok': True, 'nombre': nombre} if nombre else {'ok': False})
|
||||||
|
|
||||||
|
def invitados(self):
|
||||||
|
try:
|
||||||
|
n = int(self.headers.get('Content-Length', '0'))
|
||||||
|
if n <= 0 or n > 128 * 1024:
|
||||||
|
return self.reply(413, {'ok': False, 'error': 'La ficha es demasiado larga.'})
|
||||||
|
d = json.loads(self.rfile.read(n))
|
||||||
|
except Exception:
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'Petición no válida.'})
|
||||||
|
if d.get('web') or (isinstance(d.get('t'), (int, float)) and d['t'] < 20000):
|
||||||
|
return self.reply(200, {'ok': True}) # trampa o demasiado rápido: no se envía
|
||||||
|
if limited(guest_hits, self.client_ip(), 8):
|
||||||
|
return self.reply(429, {'ok': False, 'error': 'Demasiados envíos. Inténtalo más tarde.'})
|
||||||
|
nombre = invites().get(str(d.get('k') or '').strip())
|
||||||
|
if not nombre:
|
||||||
|
return self.reply(403, {'ok': False, 'error': 'Enlace no válido o caducado.'})
|
||||||
|
if d.get('consent') is not True:
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'Debes aceptar las condiciones de participación.'})
|
||||||
|
resp = d.get('resp') if isinstance(d.get('resp'), dict) else {}
|
||||||
|
email = clean(d.get('email'), 200)
|
||||||
|
if email and not EMAIL_RE.match(email):
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'El correo no parece válido.'})
|
||||||
|
lineas, seccion = [], None
|
||||||
|
for q in GUEST_Q:
|
||||||
|
ans = str(resp.get(q['id']) or '').replace('\r', '').strip()[:3000]
|
||||||
|
if q['req'] and not ans:
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'Faltan respuestas obligatorias.'})
|
||||||
|
if q['sec'] != seccion:
|
||||||
|
seccion = q['sec']
|
||||||
|
lineas += ['', f'== {seccion} ==']
|
||||||
|
lineas += ['', f"• {q['t']}", ' ' + (ans.replace('\n', '\n ') if ans else '(sin respuesta)')]
|
||||||
|
cabecera = [f'Ficha del invitado: {nombre}', 'Recibida: ' + time.strftime('%d/%m/%Y %H:%M', time.localtime()),
|
||||||
|
f'Correo (opcional): {email or "—"}',
|
||||||
|
'Aceptó las condiciones de participación y la política de privacidad: sí']
|
||||||
|
try:
|
||||||
|
send(f'[Invitado] {clean(nombre, 80)} · ficha del podcast', email, '\n'.join(cabecera + lineas) + '\n')
|
||||||
|
except RuntimeError:
|
||||||
|
return self.reply(503, {'ok': False, 'error': 'El formulario no está disponible ahora mismo.'})
|
||||||
|
except Exception as exc:
|
||||||
|
print('error enviando ficha:', type(exc).__name__, file=sys.stderr, flush=True)
|
||||||
|
return self.reply(502, {'ok': False, 'error': 'No se pudo enviar. Inténtalo más tarde.'})
|
||||||
|
self.reply(200, {'ok': True})
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
|
if self.path.split('?')[0] == '/api/invitados':
|
||||||
|
return self.invitados()
|
||||||
if self.path not in ('/api/contacto', '/api/patrocinio'):
|
if self.path not in ('/api/contacto', '/api/patrocinio'):
|
||||||
return self.reply(404, {'ok': False})
|
return self.reply(404, {'ok': False})
|
||||||
patro = self.path == '/api/patrocinio'
|
patro = self.path == '/api/patrocinio'
|
||||||
|
|||||||
Reference in New Issue
Block a user