stranck-dev in drew-dev #17

Merged
drew merged 8 commits from stranck-dev into drew-dev 2024-02-13 15:47:38 +00:00
14 changed files with 232 additions and 73 deletions

4
.gitignore vendored
View File

@ -165,3 +165,7 @@ res/rooms/*
config.py config.py
furizon_webinit_riverside2023.tar.gz furizon_webinit_riverside2023.tar.gz
diomerdas diomerdas
furizon.net/site/*
furizon.net.zip
stuff/secrets.py
backups/*

29
app.py
View File

@ -1,7 +1,7 @@
from sanic import Sanic, response, exceptions from sanic import Sanic, response, exceptions
from sanic.response import text, html, redirect, raw from sanic.response import text, html, redirect, raw
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from time import time from time import time, sleep
import httpx import httpx
from os.path import join from os.path import join
from ext import * from ext import *
@ -12,7 +12,9 @@ from io import BytesIO
from asyncio import Queue from asyncio import Queue
from messages import LOCALES from messages import LOCALES
import sqlite3 import sqlite3
from sanic.log import logger import requests
import sys
from sanic.log import logger, logging
app = Sanic(__name__) app = Sanic(__name__)
app.static("/res", "res/") app.static("/res", "res/")
@ -45,6 +47,7 @@ async def clear_session(request, exception):
@app.before_server_start @app.before_server_start
async def main_start(*_): async def main_start(*_):
logger.info(f"[{app.name}] >>>>>> main_start <<<<<<") logger.info(f"[{app.name}] >>>>>> main_start <<<<<<")
logger.setLevel(LOG_LEVEL)
app.config.REQUEST_MAX_SIZE = PROPIC_MAX_FILE_SIZE * 3 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.") raise exceptions.Forbidden("You have been logged out.")
if __name__ == "__main__": 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)

View File

@ -1,10 +1,14 @@
from sanic.log import logging
LOG_LEVEL = logging.DEBUG
API_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' API_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
ORGANIZER = 'furizon' ORGANIZER = 'furizon'
EVENT_NAME = 'overlord' EVENT_NAME = 'overlord'
HOSTNAME = 'reg.furizon.net' HOSTNAME = 'reg.furizon.net'
headers = {'Host': HOSTNAME, 'Authorization': f'Token {API_TOKEN}'} 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}/" base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/"
PROPIC_DEADLINE = 9999999999 PROPIC_DEADLINE = 9999999999
@ -25,6 +29,9 @@ SMTP_HOST = 'host'
SMTP_PORT = 0 SMTP_PORT = 0
SMTP_USER = 'user' SMTP_USER = 'user'
SMTP_PASSWORD = 'pw' 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 FILL_CACHE = True
CACHE_EXPIRE_TIME = 60 * 60 * 4 CACHE_EXPIRE_TIME = 60 * 60 * 4
@ -111,11 +118,6 @@ CATEGORIES_LIST_MAP = {
'dailys': [] '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. # Create a bunch of "room" items which will get added to the order once somebody gets a room.
# Map item_name -> room capacity # Map item_name -> room capacity
ROOM_CAPACITY_MAP = { ROOM_CAPACITY_MAP = {

View File

@ -1,44 +1,94 @@
from sanic import Sanic from sanic import Sanic
from sanic.log import logger
import ssl
from ssl import SSLContext
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from messages import ROOM_ERROR_TYPES from messages import ROOM_ERROR_TYPES
import smtplib import smtplib
from messages import * from messages import *
from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD from config import *
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from threading import Timer, Lock
async def send_unconfirm_message (room_order, orders): def killSmptClient():
memberMessages = [] 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 = "" async def openSmptClient():
issues_html = "<ul>" 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: def createTimer():
if err in ROOM_ERROR_TYPES.keys(): return Timer(SMPT_CLIENT_CLOSE_TIMEOUT, killSmptClient)
issues_plain += f"{ROOM_ERROR_TYPES[err]}\n" sslLock : Lock = Lock()
issues_html += f"<li>{ROOM_ERROR_TYPES[err]}</li>" sslTimer : Timer = createTimer()
issues_html += "</ul>" sslContext : SSLContext = ssl.create_default_context()
smptSender : smtplib.SMTP = None
for member in orders: async def sendEmail(message : MIMEMultipart):
plain_body = ROOM_UNCONFIRM_TEXT['plain'].format(member.name, room_order.room_name, issues_plain) await openSmptClient()
html_body = render_email_template(ROOM_UNCONFIRM_TITLE, ROOM_UNCONFIRM_TEXT['html'].format(member.name, room_order.room_name, issues_html)) logger.debug(f"[SMPT] Sending mail {message['From']} -> {message['to']} '{message['Subject']}'")
plain_text = MIMEText(plain_body, "plain") sslLock.acquire()
html_text = MIMEText(html_body, "html") smptSender.sendmail(message['From'], message['to'], message.as_string())
message = MIMEMultipart("alternative") sslLock.release()
message.attach(plain_text)
message.attach(html_text)
message['Subject'] = '[Furizon] Your room cannot be confirmed'
message['From'] = 'Furizon <no-reply@furizon.net>'
message['To'] = f"{member.name} <{member.email}>"
memberMessages.append(message)
if len(memberMessages) == 0: return async def send_unconfirm_message(room_order, orders):
memberMessages = []
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as sender: issues_plain = ""
sender.login(SMTP_USER, SMTP_PASSWORD) issues_html = "<ul>"
for message in memberMessages:
sender.sendmail(message['From'], message['to'], message.as_string()) 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"<li>{orderStr}{ROOM_ERROR_TYPES[errId]}</li>"
issues_html += "</ul>"
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 = ""): def render_email_template(title = "", body = ""):
tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html') tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html')
return str(tpl.render(title=title, body=body)) return str(tpl.render(title=title, body=body))

View File

@ -18,7 +18,8 @@ def draw_profile (source, member, position, font, size=(170, 170), border_width=
# Draw border # Draw border
idraw.rounded_rectangle(border_loc, border_width, border_color) idraw.rounded_rectangle(border_loc, border_width, border_color)
# Draw profile picture # 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) source.paste(to_add.resize (size), profile_location)
name_len = idraw.textlength(str(member['name']), font) name_len = idraw.textlength(str(member['name']), font)
calc_size = 0 calc_size = 0

View File

@ -94,21 +94,6 @@
#clock {display:block;} #clock {display:block;}
</style> </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> </head>
<body> <body>
<div class="containedBg"></div> <div class="containedBg"></div>

View File

@ -3,4 +3,5 @@ sanic-ext
httpx httpx
Pillow Pillow
aztec_code_generator aztec_code_generator
jinja2 Jinja2
Requests

3
startup.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
python3 app.py 2>&1 | tee -a log.txt

44
stuff/runBackup.py Normal file
View File

@ -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)

32
stuff/testEmail.py Normal file
View File

@ -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")

View File

@ -20,7 +20,7 @@
h1 img {max-height:1.4em;} h1 img {max-height:1.4em;}
.notice {padding:0.8em;border-radius:3px;background:#e53935;color:#eee;} .notice {padding:0.8em;border-radius:3px;background:#e53935;color:#eee;}
.notice a {color:#eee;text-decoration:underline;} .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 > a[role=button] {padding: 0.3em 0.7em;}
td {padding-left: 0.2em;padding-right: 0.2em;} td {padding-left: 0.2em;padding-right: 0.2em;}
td > input[type=file] {margin:0;padding:0;} td > input[type=file] {margin:0;padding:0;}

View File

@ -10,13 +10,13 @@
.container { max-width: 40em; padding: 1em; margin: 0 auto; } .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; } .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;} .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; } .link { text-decoration: none; background-color: #1095c1; color: #fff; padding: 1em; border-radius: 5px; font-weight: 600; }
</style> </style>
</head> </head>
<div class="body"> <div class="body">
<div class="container"> <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> <div>
<h2 class="title">{{title}}</h2> <h2 class="title">{{title}}</h2>
<p class="main-content">{{body}}</p> <p class="main-content">{{body}}</p>

View File

@ -88,8 +88,8 @@
<details id="shuttle"> <details id="shuttle">
<summary role="button"><img src="/res/icons/bus.svg" class="icon" />Shuttle</summary> <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>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> <p style="text-align:right;"><a href="{{LOCALES['shuttle_link_url'][locale]}}" target="_blank" role="button">Book now!</a></p>
</details> </details>
<details id="barcard"> <details id="barcard">

View File

@ -154,26 +154,32 @@ async def validate_rooms(request, rooms, om):
logger.info('Validating rooms...') logger.info('Validating rooms...')
if not om: om = request.app.ctx.om 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 # Validate rooms
for order in rooms: for order in rooms:
# returns tuple (room owner order, check, room error list, room members orders)
result = await check_room(request, order, om) result = await check_room(request, order, om)
order = result[0] if(len(order.room_errors) > 0):
room_with_errors.append(result)
check = result[1] check = result[1]
if check != None and check == False: if check != None and check == False:
failed_rooms.append(result) rooms_to_unconfirm.append(result)
# End here if no room has failed check # 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.') logger.info('[ROOM VALIDATION] Every room passed the check.')
return 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 # 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: if len(failed_confirmed_rooms) == 0:
logger.info('[ROOM VALIDATION] No rooms to unconfirm.') 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 # This is not needed anymore you buy tickets already
#if quotas.get_left(len(order.room_members)) == 0: #if quotas.get_left(len(order.room_members)) == 0:
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.") # 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 bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room
for m in order.room_members: for m in order.room_members:
@ -223,20 +230,27 @@ async def check_room(request, order, om=None):
# Room user in another room # Room user in another room
if res.room_id != order.code: 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': if res.status != 'paid':
room_errors.append('unpaid') room_errors.append((res.code, 'unpaid'))
if res.bed_in_room != bed_in_room: 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: if res.daily:
room_errors.append('daily') room_errors.append((res.code, 'daily'))
if order.room_confirmed:
allOk = False
room_members.append(res) room_members.append(res)
if len(room_members) != order.room_person_no and order.room_person_no != None: 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) order.set_room_errors(room_errors)
return (order, len(room_errors) == 0, room_members) return (order, allOk, room_members)