diff --git a/.gitignore b/.gitignore
index e2b56cd..23d733b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,3 +165,7 @@ res/rooms/*
config.py
furizon_webinit_riverside2023.tar.gz
diomerdas
+furizon.net/site/*
+furizon.net.zip
+stuff/secrets.py
+backups/*
\ No newline at end of file
diff --git a/app.py b/app.py
index 5a22c98..1c81301 100644
--- a/app.py
+++ b/app.py
@@ -1,7 +1,7 @@
from sanic import Sanic, response, exceptions
from sanic.response import text, html, redirect, raw
from jinja2 import Environment, FileSystemLoader
-from time import time
+from time import time, sleep
import httpx
from os.path import join
from ext import *
@@ -12,7 +12,9 @@ from io import BytesIO
from asyncio import Queue
from messages import LOCALES
import sqlite3
-from sanic.log import logger
+import requests
+import sys
+from sanic.log import logger, logging
app = Sanic(__name__)
app.static("/res", "res/")
@@ -45,6 +47,7 @@ async def clear_session(request, exception):
@app.before_server_start
async def main_start(*_):
logger.info(f"[{app.name}] >>>>>> main_start <<<<<<")
+ logger.setLevel(LOG_LEVEL)
app.config.REQUEST_MAX_SIZE = PROPIC_MAX_FILE_SIZE * 3
@@ -184,4 +187,24 @@ async def logout(request):
raise exceptions.Forbidden("You have been logged out.")
if __name__ == "__main__":
- app.run(host="0.0.0.0", port=8188, dev=DEV_MODE, access_log=ACCESS_LOG)
+ # Wait for pretix in server reboot
+ # Using a docker configuration, pretix may be unable to talk with postgres if postgres' service started before it.
+ # To fix this issue I added a After=pretix.service to the [Unit] section of /lib/systemd/system/postgresql@.service
+ # to let it start in the correct order. The following piece of code makes sure that pretix is running and can talk to
+ # postgres before actually starting the reserved area, since this operation requires a cache-fill in startup
+ print("Waiting for pretix to be up and running", file=sys.stderr)
+ while True:
+ print("Trying connecting to pretix...", file=sys.stderr)
+ try:
+ res = requests.get(base_url_event, headers=headers)
+ res = res.json()
+ if(res['slug'] == EVENT_NAME):
+ print("Healtchecking...", file=sys.stderr)
+ res = requests.get(join(domain, "healthcheck"), headers=headers)
+ if(res.status_code == 200):
+ break
+ except:
+ pass
+ sleep(5)
+ print("Connected to pretix!", file=sys.stderr)
+ app.run(host="127.0.0.1", port=8188, dev=DEV_MODE, access_log=ACCESS_LOG)
diff --git a/config.example.py b/config.example.py
index 316e9ff..28db10e 100644
--- a/config.example.py
+++ b/config.example.py
@@ -1,10 +1,14 @@
+from sanic.log import logging
+LOG_LEVEL = logging.DEBUG
+
API_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
ORGANIZER = 'furizon'
EVENT_NAME = 'overlord'
HOSTNAME = 'reg.furizon.net'
headers = {'Host': HOSTNAME, 'Authorization': f'Token {API_TOKEN}'}
-base_url = "http://urlllllllllllllllllllll/api/v1/"
+domain = "http://urlllllllllllllllllllll/"
+base_url = "{domain}api/v1/"
base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/"
PROPIC_DEADLINE = 9999999999
@@ -25,6 +29,9 @@ SMTP_HOST = 'host'
SMTP_PORT = 0
SMTP_USER = 'user'
SMTP_PASSWORD = 'pw'
+EMAIL_SENDER_NAME = "Fantastic Furcon Wow"
+EMAIL_SENDER_MAIL = "no-reply@thisIsAFantasticFurconWowItIsWonderful.cuteFurries.ovh"
+SMPT_CLIENT_CLOSE_TIMEOUT = 60 * 15 # 15 minutes
FILL_CACHE = True
CACHE_EXPIRE_TIME = 60 * 60 * 4
@@ -111,11 +118,6 @@ CATEGORIES_LIST_MAP = {
'dailys': []
}
-SPONSORSHIP_COLOR_MAP = {
- 'super': (251, 140, 0),
- 'normal': (142, 36, 170)
-}
-
# Create a bunch of "room" items which will get added to the order once somebody gets a room.
# Map item_name -> room capacity
ROOM_CAPACITY_MAP = {
diff --git a/email_util.py b/email_util.py
index 1c7c05e..6621e66 100644
--- a/email_util.py
+++ b/email_util.py
@@ -1,44 +1,94 @@
from sanic import Sanic
+from sanic.log import logger
+import ssl
+from ssl import SSLContext
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from messages import ROOM_ERROR_TYPES
import smtplib
from messages import *
-from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD
+from config import *
from jinja2 import Environment, FileSystemLoader
+from threading import Timer, Lock
-async def send_unconfirm_message (room_order, orders):
- memberMessages = []
+def killSmptClient():
+ global sslLock
+ global sslTimer
+ global smptSender
+ sslTimer.cancel()
+ sslLock.acquire()
+ if(smptSender is not None):
+ logger.debug('[SMPT] Closing smpt client')
+ smptSender.quit() # it calls close() inside
+ smptSender = None
+ sslLock.release()
- issues_plain = ""
- issues_html = "
"
+async def openSmptClient():
+ global sslLock
+ global sslTimer
+ global sslContext
+ global smptSender
+ sslTimer.cancel()
+ sslLock.acquire()
+ if(smptSender is None):
+ logger.debug('[SMPT] Opening smpt client')
+ client : smtplib.SMTP = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
+ client.starttls(context=sslContext)
+ client.login(SMTP_USER, SMTP_PASSWORD)
+ smptSender = client
+ sslLock.release()
+ sslTimer = createTimer()
+ sslTimer.start()
- for err in room_order.room_errors:
- if err in ROOM_ERROR_TYPES.keys():
- issues_plain += f" • {ROOM_ERROR_TYPES[err]}\n"
- issues_html += f"- {ROOM_ERROR_TYPES[err]}
"
- issues_html += "
"
+def createTimer():
+ return Timer(SMPT_CLIENT_CLOSE_TIMEOUT, killSmptClient)
+sslLock : Lock = Lock()
+sslTimer : Timer = createTimer()
+sslContext : SSLContext = ssl.create_default_context()
+smptSender : smtplib.SMTP = None
- for member in orders:
- plain_body = ROOM_UNCONFIRM_TEXT['plain'].format(member.name, room_order.room_name, issues_plain)
- html_body = render_email_template(ROOM_UNCONFIRM_TITLE, ROOM_UNCONFIRM_TEXT['html'].format(member.name, room_order.room_name, issues_html))
- plain_text = MIMEText(plain_body, "plain")
- html_text = MIMEText(html_body, "html")
- message = MIMEMultipart("alternative")
- message.attach(plain_text)
- message.attach(html_text)
- message['Subject'] = '[Furizon] Your room cannot be confirmed'
- message['From'] = 'Furizon '
- message['To'] = f"{member.name} <{member.email}>"
- memberMessages.append(message)
+async def sendEmail(message : MIMEMultipart):
+ await openSmptClient()
+ logger.debug(f"[SMPT] Sending mail {message['From']} -> {message['to']} '{message['Subject']}'")
+ sslLock.acquire()
+ smptSender.sendmail(message['From'], message['to'], message.as_string())
+ sslLock.release()
- if len(memberMessages) == 0: return
+async def send_unconfirm_message(room_order, orders):
+ memberMessages = []
- with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as sender:
- sender.login(SMTP_USER, SMTP_PASSWORD)
- for message in memberMessages:
- sender.sendmail(message['From'], message['to'], message.as_string())
+ issues_plain = ""
+ issues_html = ""
+
+ for err in room_order.room_errors:
+ errId = err[1]
+ order = err[0]
+ orderStr = ""
+ if order is not None:
+ orderStr = f"{order}: "
+ if errId in ROOM_ERROR_TYPES.keys():
+ issues_plain += f" • {orderStr}{ROOM_ERROR_TYPES[errId]}\n"
+ issues_html += f"- {orderStr}{ROOM_ERROR_TYPES[errId]}
"
+ issues_html += "
"
+
+ for member in orders:
+ plain_body = ROOM_UNCONFIRM_TEXT['plain'].format(member.name, room_order.room_name, issues_plain)
+ html_body = render_email_template(ROOM_UNCONFIRM_TITLE, ROOM_UNCONFIRM_TEXT['html'].format(member.name, room_order.room_name, issues_html))
+ plain_text = MIMEText(plain_body, "plain")
+ html_text = MIMEText(html_body, "html")
+ message = MIMEMultipart("alternative")
+ message.attach(plain_text)
+ message.attach(html_text)
+ message['Subject'] = f'[{EMAIL_SENDER_NAME}] Your room cannot be confirmed'
+ message['From'] = f'{EMAIL_SENDER_NAME} <{EMAIL_SENDER_MAIL}>'
+ message['To'] = f"{member.name} <{member.email}>"
+ memberMessages.append(message)
+
+ if len(memberMessages) == 0: return
+
+ for message in memberMessages:
+ await sendEmail(message)
def render_email_template(title = "", body = ""):
- tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html')
- return str(tpl.render(title=title, body=body))
\ No newline at end of file
+ tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html')
+ return str(tpl.render(title=title, body=body))
\ No newline at end of file
diff --git a/image_util.py b/image_util.py
index 75a7a07..6797714 100644
--- a/image_util.py
+++ b/image_util.py
@@ -18,7 +18,8 @@ def draw_profile (source, member, position, font, size=(170, 170), border_width=
# Draw border
idraw.rounded_rectangle(border_loc, border_width, border_color)
# Draw profile picture
- with Image.open(f'res/propic/{member['propic'] or 'default.png'}') as to_add:
+ fileName = member['propic'] or 'default.png'
+ with Image.open(f'res/propic/{fileName}') as to_add:
source.paste(to_add.resize (size), profile_location)
name_len = idraw.textlength(str(member['name']), font)
calc_size = 0
diff --git a/reg.furizon.net/index.html b/reg.furizon.net/index.html
index c07a871..9815975 100644
--- a/reg.furizon.net/index.html
+++ b/reg.furizon.net/index.html
@@ -94,21 +94,6 @@
#clock {display:block;}
-
-
-
diff --git a/requirements.txt b/requirements.txt
index bfa4da3..c2ae761 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,5 @@ sanic-ext
httpx
Pillow
aztec_code_generator
-jinja2
+Jinja2
+Requests
\ No newline at end of file
diff --git a/startup.sh b/startup.sh
new file mode 100644
index 0000000..1330c0b
--- /dev/null
+++ b/startup.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+python3 app.py 2>&1 | tee -a log.txt
diff --git a/stuff/runBackup.py b/stuff/runBackup.py
new file mode 100644
index 0000000..740a1b3
--- /dev/null
+++ b/stuff/runBackup.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+
+from os import listdir, remove
+from os.path import isfile, join
+import datetime
+import subprocess
+
+PRETIX_BACKUP = False
+WEBINT_BACKUP = False
+
+BACKUP_DIR_PRETIX = "/home/pretix/backups/"
+BACKUP_DIR_WEBINT = "/home/webint/backups/"
+
+MAX_FILE_NO = 14
+
+COMMAND_PRETIX_POSTGRES = "pg_dump -F p pretix | gzip > %s" # Restore with psql -f %s
+COMMAND_PRETIX_DATA = "tar -cf %s /var/pretix-data" # Restore with tar -xvf %s. To make .secret readable I used setfacl -m u:pretix:r /var/pretix-data/.secret
+COMMAND_WEBINT = "tar -cf %s /home/webint/furizon_webint" # Restore with tar -xvf %s
+
+
+def deleteOlder(path : str, prefix : str, postfix : str):
+ backupFileNames = sorted([f for f in listdir(path) if (isfile(join(path, f)) and f.startswith(prefix) and f.endswith(postfix))])
+ while(len(backupFileNames) > MAX_FILE_NO):
+ print(f"Removing {backupFileNames[0]}")
+ remove(join(path, backupFileNames[0]))
+ backupFileNames.pop(0)
+
+def genFileName(prefix : str, postfix : str):
+ return prefix + "_" + datetime.datetime.now(datetime.UTC).strftime('%Y%m%d-%H%M%S') + "_" + postfix
+
+def runBackup(prefix : str, postfix : str, path : str, command : str):
+ deleteOlder(path, prefix, postfix)
+ name = join(path, genFileName(prefix, postfix))
+ process = subprocess.Popen(command % name, shell=True)
+ process.wait()
+
+
+
+if(PRETIX_BACKUP):
+ runBackup("pretix_postres", "backup.sql.gz", join(BACKUP_DIR_PRETIX, "postgres"), COMMAND_PRETIX_POSTGRES)
+ runBackup("pretix_data", "backup.tar.gz", join(BACKUP_DIR_PRETIX, "data"), COMMAND_PRETIX_DATA)
+
+if(WEBINT_BACKUP):
+ runBackup("webint_full", "backup.tar.gz", BACKUP_DIR_WEBINT, COMMAND_WEBINT)
\ No newline at end of file
diff --git a/stuff/testEmail.py b/stuff/testEmail.py
new file mode 100644
index 0000000..feacda7
--- /dev/null
+++ b/stuff/testEmail.py
@@ -0,0 +1,32 @@
+import smtplib
+import ssl
+
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+
+from secrets import *
+
+SMTP_HOST = 'smtp.office365.com'
+SMTP_USER = 'webservice@furizon.net'
+SMTP_PORT = 587
+
+plain_body = "Test aaaa"
+
+plain_text = MIMEText(plain_body, "plain")
+message = MIMEMultipart("alternative")
+message.attach(plain_text)
+message['Subject'] = '[Furizon] This is a test!'
+message['From'] = 'Furizon '
+message['To'] = f"Luca Sorace "
+
+print("Start")
+context = ssl.create_default_context()
+print("Context created")
+with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as sender:
+ print("Created sender obj")
+ sender.starttls(context=context)
+ print("Tls started")
+ sender.login(SMTP_USER, SMTP_PASSWORD)
+ print("Logged in")
+ print(sender.sendmail(message['From'], message['to'], message.as_string()))
+ print("Mail sent")
\ No newline at end of file
diff --git a/tpl/base.html b/tpl/base.html
index b2e464f..a832e47 100644
--- a/tpl/base.html
+++ b/tpl/base.html
@@ -20,7 +20,7 @@
h1 img {max-height:1.4em;}
.notice {padding:0.8em;border-radius:3px;background:#e53935;color:#eee;}
.notice a {color:#eee;text-decoration:underline;}
- .container {max-width:40em;padding:1em;box-sizing:border-box;}
+ .container {max-width:50em;padding:1em;box-sizing:border-box;}
td > a[role=button] {padding: 0.3em 0.7em;}
td {padding-left: 0.2em;padding-right: 0.2em;}
td > input[type=file] {margin:0;padding:0;}
diff --git a/tpl/email/comunication.html b/tpl/email/comunication.html
index e955d32..ffd288e 100644
--- a/tpl/email/comunication.html
+++ b/tpl/email/comunication.html
@@ -10,13 +10,13 @@
.container { max-width: 40em; padding: 1em; margin: 0 auto; }
.title { font-size: 1.75em; margin-bottom: 1.2em; color: #e1e6eb; margin-top: 0; font-family: sans-serif; }
.main-content { margin-top: 0; font-style: normal; font-weight: 400; font-family: sans-serif;}
- .con-logo { height:2em;}
+ .con-logo { height:3em;}
.link { text-decoration: none; background-color: #1095c1; color: #fff; padding: 1em; border-radius: 5px; font-weight: 600; }
-
+
{{title}}
{{body}}
diff --git a/tpl/welcome.html b/tpl/welcome.html
index 1c06f89..89cbd50 100644
--- a/tpl/welcome.html
+++ b/tpl/welcome.html
@@ -88,8 +88,8 @@
Shuttle
- This year we teamed up with VisitFiemme to take our guests to the convention.
- Book now
+ This year, a shuttle service operated by the tourism company of Val di Fiemme will be available. The shuttle service will consist of a bus serving the convention, with scheduled stops at major airports and train stations. More informations in the dedicated page.
+ Book now!
diff --git a/utils.py b/utils.py
index e8da527..86f5b2f 100644
--- a/utils.py
+++ b/utils.py
@@ -154,26 +154,32 @@ async def validate_rooms(request, rooms, om):
logger.info('Validating rooms...')
if not om: om = request.app.ctx.om
- failed_rooms = []
+ # rooms_to_unconfirm is the room that MUST be unconfirmed, room_with_errors is a less strict set containing all rooms with kind-ish errors
+ rooms_to_unconfirm = []
+ room_with_errors = []
# Validate rooms
for order in rooms:
- # returns tuple (room owner order, check, room error list, room members orders)
result = await check_room(request, order, om)
- order = result[0]
+ if(len(order.room_errors) > 0):
+ room_with_errors.append(result)
check = result[1]
if check != None and check == False:
- failed_rooms.append(result)
+ rooms_to_unconfirm.append(result)
# End here if no room has failed check
- if len(failed_rooms) == 0:
+ if len(room_with_errors) == 0:
logger.info('[ROOM VALIDATION] Every room passed the check.')
return
- logger.warning(f'[ROOM VALIDATION] Room validation failed for orders: %s', list(map(lambda rf: rf[0].code, failed_rooms)))
+ roomErrListSrts = []
+ for fr in room_with_errors:
+ for error in fr[0].room_errors:
+ roomErrListSrts.append(f"[ROOM VALIDATION] [ERR] Parent room: {fr[0].code} {'C' if fr[0].room_confirmed else 'N'} | Order {error[0] if error[0] else '-----'} with code {error[1]}")
+ logger.warning(f'[ROOM VALIDATION] Room validation failed for orders: \n%s', "\n".join(roomErrListSrts))
# Get confirmed rooms that fail validation
- failed_confirmed_rooms = list(filter(lambda fr: (fr[0].room_confirmed == True), failed_rooms))
+ failed_confirmed_rooms = list(filter(lambda fr: (fr[0].room_confirmed == True), rooms_to_unconfirm))
if len(failed_confirmed_rooms) == 0:
logger.info('[ROOM VALIDATION] No rooms to unconfirm.')
@@ -213,6 +219,7 @@ async def check_room(request, order, om=None):
# This is not needed anymore you buy tickets already
#if quotas.get_left(len(order.room_members)) == 0:
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
+ allOk = True
bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room
for m in order.room_members:
@@ -223,20 +230,27 @@ async def check_room(request, order, om=None):
# Room user in another room
if res.room_id != order.code:
- room_errors.append('room_id_mismatch')
+ room_errors.append((res.code, 'room_id_mismatch'))
+ allOk = False
if res.status != 'paid':
- room_errors.append('unpaid')
+ room_errors.append((res.code, 'unpaid'))
if res.bed_in_room != bed_in_room:
- room_errors.append('type_mismatch')
+ room_errors.append((res.code, 'type_mismatch'))
+ if order.room_confirmed:
+ allOk = False
if res.daily:
- room_errors.append('daily')
+ room_errors.append((res.code, 'daily'))
+ if order.room_confirmed:
+ allOk = False
room_members.append(res)
if len(room_members) != order.room_person_no and order.room_person_no != None:
- room_errors.append('capacity_mismatch')
+ room_errors.append((None, 'capacity_mismatch'))
+ if order.room_confirmed:
+ allOk = False
order.set_room_errors(room_errors)
- return (order, len(room_errors) == 0, room_members)
\ No newline at end of file
+ return (order, allOk, room_members)