drew-dev #19
|
@ -165,3 +165,7 @@ res/rooms/*
|
|||
config.py
|
||||
furizon_webinit_riverside2023.tar.gz
|
||||
diomerdas
|
||||
furizon.net/site/*
|
||||
furizon.net.zip
|
||||
stuff/secrets.py
|
||||
backups/*
|
29
app.py
29
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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -1,11 +1,58 @@
|
|||
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
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
async def send_unconfirm_message(room_order, orders):
|
||||
memberMessages = []
|
||||
|
@ -14,9 +61,14 @@ async def send_unconfirm_message (room_order, orders):
|
|||
issues_html = "<ul>"
|
||||
|
||||
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"<li>{ROOM_ERROR_TYPES[err]}</li>"
|
||||
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"<li>{orderStr}{ROOM_ERROR_TYPES[errId]}</li>"
|
||||
issues_html += "</ul>"
|
||||
|
||||
for member in orders:
|
||||
|
@ -27,17 +79,15 @@ async def send_unconfirm_message (room_order, orders):
|
|||
message = MIMEMultipart("alternative")
|
||||
message.attach(plain_text)
|
||||
message.attach(html_text)
|
||||
message['Subject'] = '[Furizon] Your room cannot be confirmed'
|
||||
message['From'] = 'Furizon <no-reply@furizon.net>'
|
||||
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
|
||||
|
||||
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())
|
||||
await sendEmail(message)
|
||||
|
||||
def render_email_template(title = "", body = ""):
|
||||
tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -94,21 +94,6 @@
|
|||
#clock {display:block;}
|
||||
|
||||
</style>
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://y.foxo.me/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '2']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="containedBg"></div>
|
||||
|
|
|
@ -3,4 +3,5 @@ sanic-ext
|
|||
httpx
|
||||
Pillow
|
||||
aztec_code_generator
|
||||
jinja2
|
||||
Jinja2
|
||||
Requests
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
python3 app.py 2>&1 | tee -a log.txt
|
|
@ -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)
|
|
@ -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 <webservice@furizon.net>'
|
||||
message['To'] = f"Luca Sorace <strdjn@gmail.com>"
|
||||
|
||||
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")
|
|
@ -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;}
|
||||
|
|
|
@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<div class="body">
|
||||
<div class="container">
|
||||
<img src="https://reg.furizon.net/res/furizon.png" class="con-logo">
|
||||
<img src="https://reg.furizon.net/res/furizon.png" alt="con_logo" title="con_logo" class="con-logo">
|
||||
<div>
|
||||
<h2 class="title">{{title}}</h2>
|
||||
<p class="main-content">{{body}}</p>
|
||||
|
|
|
@ -88,8 +88,8 @@
|
|||
|
||||
<details id="shuttle">
|
||||
<summary role="button"><img src="/res/icons/bus.svg" class="icon" />Shuttle</summary>
|
||||
<p>This year we teamed up with VisitFiemme to take our guests to the convention.</p>
|
||||
<p style="text-align:right;"><a href="{{LOCALES['shuttle_link_url'][locale]}}" target="_blank" role="button">Book now</a></p>
|
||||
<p>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 <a href="https://furizon.net/furizon-overlord/furizon-overlord-shuttle-bus/">in the dedicated page.</a></p>
|
||||
<p style="text-align:right;"><a href="{{LOCALES['shuttle_link_url'][locale]}}" target="_blank" role="button">Book now!</a></p>
|
||||
</details>
|
||||
|
||||
<details id="barcard">
|
||||
|
|
40
utils.py
40
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)
|
||||
return (order, allOk, room_members)
|
||||
|
|
Loading…
Reference in New Issue