drew-dev #21

Merged
stranck merged 2 commits from drew-dev into stranck-dev 2024-03-04 09:36:41 +00:00
18 changed files with 394 additions and 217 deletions
Showing only changes of commit edc306c6c1 - Show all commits

View File

@ -1,4 +1,5 @@
from sanic import response, redirect, Blueprint, exceptions from sanic import response, redirect, Blueprint, exceptions
from email_util import send_missing_propic_message
from room import unconfirm_room_by_order from room import unconfirm_room_by_order
from config import * from config import *
from utils import * from utils import *
@ -20,7 +21,8 @@ async def credentials_check(request: Request):
@bp.get('/cache/clear') @bp.get('/cache/clear')
async def clear_cache(request, order:Order): async def clear_cache(request, order:Order):
await request.app.ctx.om.fill_cache() success = await request.app.ctx.om.fill_cache()
if not success: raise exceptions.ServerError("An error occurred while loading the cache")
return redirect(f'/manage/admin') return redirect(f'/manage/admin')
@bp.get('/loginas/<code>') @bp.get('/loginas/<code>')
@ -40,8 +42,8 @@ async def login_as(request, code, order:Order):
@bp.get('/room/verify') @bp.get('/room/verify')
async def verify_rooms(request, order:Order): async def verify_rooms(request, order:Order):
already_checked = await request.app.ctx.om.update_cache() already_checked, success = await request.app.ctx.om.update_cache()
if not already_checked: if not already_checked and success:
orders = filter(lambda x: x.status not in ['c', 'e'] and x.room_id == x.code, request.app.ctx.om.cache.values()) orders = filter(lambda x: x.status not in ['c', 'e'] and x.room_id == x.code, request.app.ctx.om.cache.values())
await validate_rooms(request, orders, None) await validate_rooms(request, orders, None)
return redirect(f'/manage/admin') return redirect(f'/manage/admin')
@ -82,3 +84,18 @@ async def rename_room(request, code, order:Order):
await dOrder.edit_answer("room_name", name) await dOrder.edit_answer("room_name", name)
await dOrder.send_answers() await dOrder.send_answers()
return redirect(f'/manage/nosecount') return redirect(f'/manage/nosecount')
@bp.get('/propic/remind')
async def propic_remind_missing(request, order:Order):
await clear_cache(request, order)
orders = request.app.ctx.om.cache.values()
order: Order
for order in orders:
missingPropic = order.propic is None
missingFursuitPropic = order.is_fursuiter and order.propic_fursuiter is None
if(missingPropic or missingFursuitPropic):
# print(f"{order.code}: prp={missingPropic} fpr={missingFursuitPropic} - {order.name}")
await send_missing_propic_message(order, missingPropic, missingFursuitPropic)
return redirect(f'/manage/admin')

66
app.py
View File

@ -16,6 +16,9 @@ import requests
import sys import sys
from sanic.log import logger, logging, access_logger from sanic.log import logger, logging, access_logger
from metrics import * from metrics import *
from email_util import killSmptClient
import pretixClient
import traceback
app = Sanic(__name__) app = Sanic(__name__)
app.static("/res", "res/") app.static("/res", "res/")
@ -35,17 +38,27 @@ from admin import bp as admin_bp
app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp]) app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp])
@app.exception(exceptions.SanicException)
async def clear_session(request, exception):
logger.warning(f"{request} -> {exception}")
tpl = app.ctx.tpl.get_template('error.html')
r = html(tpl.render(exception=exception))
if exception.status_code == 403: async def clear_session(response):
r.delete_cookie("foxo_code") response.delete_cookie("foxo_code")
r.delete_cookie("foxo_secret") response.delete_cookie("foxo_secret")
@app.exception(exceptions.SanicException if DEV_MODE else Exception)
async def handleException(request, exception):
incErrorNo()
logger.warning(f"{request} -> {exception}")
statusCode = exception.status_code if hasattr(exception, 'status_code') else 500
try:
tpl = app.ctx.tpl.get_template('error.html')
r = html(tpl.render(exception=exception, status_code=statusCode))
except:
traceback.print_exc()
if statusCode == 403:
clear_session(r)
return r return r
@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 <<<<<<")
@ -56,7 +69,10 @@ async def main_start(*_):
app.ctx.om = OrderManager() app.ctx.om = OrderManager()
if FILL_CACHE: if FILL_CACHE:
await app.ctx.om.update_cache() checked, success = await app.ctx.om.update_cache(check_itemsQuestions=True)
if checked and not success:
logger.error(f"[{app.name}] Failure in app startup: An error occurred while loading items or questions or cache.")
app.stop()
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db') app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
@ -90,18 +106,16 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None):
if order and order.code != code: order = None if order and order.code != code: order = None
if not order: if not order:
async with httpx.AsyncClient() as client: res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None)
incPretixRead()
res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers)
if res.status_code != 200: if res.status_code != 200:
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.") raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
res = res.json() res = res.json()
if secret != res['secret']: if secret != res['secret']:
raise exceptions.Forbidden("The secret part of the url is not correct. Check your E-Mail for the correct link, or contact support!") raise exceptions.Forbidden("The secret part of the url is not correct. Check your E-Mail for the correct link, or contact support!")
r.cookies['foxo_code'] = code r.cookies['foxo_code'] = code
r.cookies['foxo_secret'] = secret r.cookies['foxo_secret'] = secret
return r return r
@app.route("/manage/privacy") @app.route("/manage/privacy")
@ -155,11 +169,9 @@ async def download_ticket(request, order: Order):
if not order.status != 'confirmed': if not order.status != 'confirmed':
raise exceptions.Forbidden("You are not allowed to download this ticket.") raise exceptions.Forbidden("You are not allowed to download this ticket.")
async with httpx.AsyncClient() as client: res = await pretixClient.get(f"orders/{order.code}/download/pdf/", expectedStatusCodes=[200, 404, 409, 403])
incPretixRead()
res = await client.get(join(base_url_event, f"orders/{order.code}/download/pdf/"), headers=headers)
if res.status_code == 409: if res.status_code == 409 or res.status_code == 404:
raise exceptions.SanicException("Your ticket is still being generated. Please try again later!", status_code=res.status_code) raise exceptions.SanicException("Your ticket is still being generated. Please try again later!", status_code=res.status_code)
elif res.status_code == 403: elif res.status_code == 403:
raise exceptions.SanicException("You can download your ticket only after the order has been confirmed and paid. Try later!", status_code=400) raise exceptions.SanicException("You can download your ticket only after the order has been confirmed and paid. Try later!", status_code=400)
@ -191,6 +203,10 @@ async def logout(request):
raise exceptions.Forbidden("You have been logged out.") raise exceptions.Forbidden("You have been logged out.")
@app.signal("server.shutdown.before")
async def sigintHandler(app, loop):
killSmptClient()
@app.get(METRICS_PATH) @app.get(METRICS_PATH)
async def metrics(request): async def metrics(request):
return text(getMetricsText() + "\n" + getRoomCountersText(request)) return text(getMetricsText() + "\n" + getRoomCountersText(request))
@ -208,7 +224,7 @@ if __name__ == "__main__":
# to let it start in the correct order. The following piece of code makes sure that pretix is running and can talk to # 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 # 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) print("Waiting for pretix to be up and running", file=sys.stderr)
while True: while not SKIP_HEALTHCHECK:
print("Trying connecting to pretix...", file=sys.stderr) print("Trying connecting to pretix...", file=sys.stderr)
try: try:
incPretixRead() incPretixRead()
@ -218,7 +234,7 @@ if __name__ == "__main__":
print("Healtchecking...", file=sys.stderr) print("Healtchecking...", file=sys.stderr)
incPretixRead() incPretixRead()
res = requests.get(join(domain, "healthcheck"), headers=headers) res = requests.get(join(domain, "healthcheck"), headers=headers)
if(res.status_code == 200 or SKIP_HEALTHCHECK): if(res.status_code == 200):
break break
except: except:
pass pass

View File

@ -12,6 +12,7 @@ from time import time
from urllib.parse import unquote from urllib.parse import unquote
import json import json
from metrics import * from metrics import *
import pretixClient
bp = Blueprint("checkin", url_prefix="/checkin") bp = Blueprint("checkin", url_prefix="/checkin")
@ -64,9 +65,7 @@ async def do_checkin(request):
await order.send_answers() await order.send_answers()
if not order.checked_in: if not order.checked_in:
async with httpx.AsyncClient() as client: await pretixClient.post("", baseUrl=base_url_event.replace(f'events/{EVENT_NAME}/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]})
incPretixWrite()
res = await client.post(base_url_event.replace(f'events/{EVENT_NAME}/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]}, headers=headers)
tpl = request.app.ctx.tpl.get_template('checkin_3.html') tpl = request.app.ctx.tpl.get_template('checkin_3.html')
return html(tpl.render(order=order, room_owner=room_owner, roommates=roommates)) return html(tpl.render(order=order, room_owner=room_owner, roommates=roommates))

View File

@ -21,6 +21,11 @@ PROPIC_MIN_SIZE = (125, 125) # (Width, Height)
TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
TG_CHAT_ID = -1234567 TG_CHAT_ID = -1234567
import httpx
# Number of tries for a request to the pretix's backend
PRETIX_REQUESTS_MAX = 3
PRETIX_REQUESTS_TIMEOUT = httpx.Timeout(15.0, read=30.0, connect=45.0, pool=None) # Timeout for httpx requests in seconds
# These order codes have additional functions. # These order codes have additional functions.
ADMINS = ['XXXXX', 'YYYYY'] ADMINS = ['XXXXX', 'YYYYY']
# A list of staff_roles # A list of staff_roles

View File

@ -54,6 +54,13 @@ async def sendEmail(message : MIMEMultipart):
smptSender.sendmail(message['From'], message['to'], message.as_string()) smptSender.sendmail(message['From'], message['to'], message.as_string())
sslLock.release() sslLock.release()
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))
async def send_unconfirm_message(room_order, orders): async def send_unconfirm_message(room_order, orders):
memberMessages = [] memberMessages = []
@ -72,8 +79,8 @@ async def send_unconfirm_message(room_order, orders):
issues_html += "</ul>" issues_html += "</ul>"
for member in orders: for member in orders:
plain_body = ROOM_UNCONFIRM_TEXT['plain'].format(member.name, room_order.room_name, issues_plain) plain_body = EMAILS_TEXT["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)) html_body = render_email_template(EMAILS_TEXT["ROOM_UNCONFIRM_TITLE"], EMAILS_TEXT["ROOM_UNCONFIRM_TEXT"]['html'].format(member.name, room_order.room_name, issues_html))
plain_text = MIMEText(plain_body, "plain") plain_text = MIMEText(plain_body, "plain")
html_text = MIMEText(html_body, "html") html_text = MIMEText(html_body, "html")
message = MIMEMultipart("alternative") message = MIMEMultipart("alternative")
@ -89,6 +96,22 @@ async def send_unconfirm_message(room_order, orders):
for message in memberMessages: for message in memberMessages:
await sendEmail(message) await sendEmail(message)
def render_email_template(title = "", body = ""): async def send_missing_propic_message(order, missingPropic, missingFursuitPropic):
tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html') t = []
return str(tpl.render(title=title, body=body)) if(missingPropic): t.append("your propic")
if(missingFursuitPropic): t.append("your fursuit's badge")
missingText = " and ".join(t)
plain_body = EMAILS_TEXT["MISSING_PROPIC_TEXT"]['plain'].format(order.name, missingText)
html_body = render_email_template(EMAILS_TEXT["MISSING_PROPIC_TITLE"], EMAILS_TEXT["MISSING_PROPIC_TEXT"]['html'].format(order.name, missingText))
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}] You haven't uploaded your badges yet!"
message['From'] = f'{EMAIL_SENDER_NAME} <{EMAIL_SENDER_MAIL}>'
message['To'] = f"{order.name} <{order.email}>"
await sendEmail(message)

217
ext.py
View File

@ -10,6 +10,9 @@ from sanic.log import logger
from time import time from time import time
from metrics import * from metrics import *
import asyncio import asyncio
from threading import Lock
import pretixClient
import traceback
@dataclass @dataclass
class Order: class Order:
@ -156,14 +159,12 @@ class Order:
async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes): async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes):
if(mimeType != None and data != None): if(mimeType != None and data != None):
async with httpx.AsyncClient() as client: localHeaders = dict(headers)
localHeaders = dict(headers) localHeaders['Content-Type'] = mimeType
localHeaders['Content-Type'] = mimeType localHeaders['Content-Disposition'] = f'attachment; filename="{fileName}"'
localHeaders['Content-Disposition'] = f'attachment; filename="{fileName}"' res = await pretixClient.post("upload", baseUrl=base_url, headers=localHeaders, content=data)
incPretixWrite() res = res.json()
res = await client.post(join(base_url, 'upload'), headers=localHeaders, content=data) await self.edit_answer(name, res['id'])
res = res.json()
await self.edit_answer(name, res['id'])
else: else:
await self.edit_answer(name, None) await self.edit_answer(name, None)
self.loadAns() self.loadAns()
@ -184,11 +185,8 @@ class Order:
break break
if (not found) and (new_answer is not None): if (not found) and (new_answer is not None):
res = await pretixClient.get("questions/")
async with httpx.AsyncClient() as client: res = res.json()
incPretixRead()
res = await client.get(join(base_url_event, 'questions/'), headers=headers)
res = res.json()
for r in res['results']: for r in res['results']:
if r['identifier'] != name: continue if r['identifier'] != name: continue
@ -201,32 +199,30 @@ class Order:
self.loadAns() self.loadAns()
async def send_answers(self): async def send_answers(self):
async with httpx.AsyncClient() as client: if DEV_MODE and EXTRA_PRINTS: logger.debug("[ANSWER POST] POSITION ID IS %s", self.position_id)
if DEV_MODE and EXTRA_PRINTS: logger.debug("[ANSWER POST] POSITION ID IS %s", self.position_id)
for i, ans in enumerate(self.answers): for i, ans in enumerate(self.answers):
if TYPE_OF_QUESTIONS[ans['question']] == QUESTION_TYPES["multiple_choice_from_list"]: # if multiple choice if TYPE_OF_QUESTIONS[ans['question']] == QUESTION_TYPES["multiple_choice_from_list"]: # if multiple choice
identifier = ans['question_identifier'] identifier = ans['question_identifier']
if self.ans(identifier) == "": #if empty answer if self.ans(identifier) == "": #if empty answer
await self.edit_answer(identifier, None) await self.edit_answer(identifier, None)
# Fix for karaoke fields # Fix for karaoke fields
#if ans['question'] == 40: #if ans['question'] == 40:
# del self.answers[i]['options'] # del self.answers[i]['options']
# del self.answers[i]['option_identifiers'] # del self.answers[i]['option_identifiers']
incPretixWrite() res = await pretixClient.patch(f'orderpositions/{self.position_id}/', json={'answers': self.answers}, expectedStatusCodes=None)
res = await client.patch(join(base_url_event, f'orderpositions/{self.position_id}/'), headers=headers, json={'answers': self.answers})
if res.status_code != 200: if res.status_code != 200:
for ans, err in zip(self.answers, res.json()['answers']): for ans, err in zip(self.answers, res.json()['answers']):
if err: if err:
logger.error ('[ANSWERS SENDING] ERROR ON %s %s', ans, err) logger.error ('[ANSWERS SENDING] ERROR ON %s %s', ans, err)
raise exceptions.ServerError('There has been an error while updating this answers.') raise exceptions.ServerError('There has been an error while updating this answers.')
for i, ans in enumerate(self.answers): for i, ans in enumerate(self.answers):
if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']): if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']):
self.answers[i]['answer'] = "file:keep" self.answers[i]['answer'] = "file:keep"
self.pending_update = False self.pending_update = False
self.time = -1 self.time = -1
@ -247,12 +243,10 @@ class Quotas:
return 0 return 0
async def get_quotas(request: Request=None): async def get_quotas(request: Request=None):
async with httpx.AsyncClient() as client: res = await pretixClient.get('quotas/?order=id&with_availability=true')
incPretixRead() res = res.json()
res = await client.get(join(base_url_event, 'quotas/?order=id&with_availability=true'), headers=headers)
res = res.json()
return Quotas(res) return Quotas(res)
async def get_order(request: Request=None): async def get_order(request: Request=None):
await request.receive_body() await request.receive_body()
@ -261,7 +255,7 @@ async def get_order(request: Request=None):
class OrderManager: class OrderManager:
def __init__(self): def __init__(self):
self.lastCacheUpdate = 0 self.lastCacheUpdate = 0
self.updating = False self.updating : Lock = Lock()
self.empty() self.empty()
def empty(self): def empty(self):
@ -269,65 +263,90 @@ class OrderManager:
self.order_list = [] self.order_list = []
# Will fill cache once the last cache update is greater than cache expire time # Will fill cache once the last cache update is greater than cache expire time
async def update_cache(self): async def update_cache(self, check_itemsQuestions=False):
t = time() t = time()
to_return = False to_return = False
if(t - self.lastCacheUpdate > CACHE_EXPIRE_TIME and not self.updating): success = True
if(t - self.lastCacheUpdate > CACHE_EXPIRE_TIME and not self.updating.locked()):
to_return = True to_return = True
await self.fill_cache() success = await self.fill_cache(check_itemsQuestions=check_itemsQuestions)
return to_return return (to_return, success)
def add_cache(self, order): def add_cache(self, order, cache=None, orderList=None):
self.cache[order.code] = order # Extra params for dry runs
if not order.code in self.order_list: if(cache is None):
self.order_list.append(order.code) cache = self.cache
if(orderList is None):
orderList = self.order_list
def remove_cache(self, code): cache[order.code] = order
if code in self.cache: if not order.code in orderList:
del self.cache[code] orderList.append(order.code)
self.order_list.remove(code)
async def fill_cache(self): def remove_cache(self, code, cache=None, orderList=None):
# Extra params for dry runs
if(cache is None):
cache = self.cache
if(orderList is None):
orderList = self.order_list
if code in cache:
del cache[code]
orderList.remove(code)
async def fill_cache(self, check_itemsQuestions=False) -> bool:
# Check cache lock # Check cache lock
if self.updating == True: return self.updating.acquire()
# Set cache lock
self.updating = True
start_time = time() start_time = time()
logger.info("[CACHE] Filling cache...") logger.info("[CACHE] Filling cache...")
# Index item's ids # Index item's ids
await load_items() r = await load_items()
if(not r and check_itemsQuestions):
logger.error("[CACHE] Items were not loading correctly. Aborting filling cache...")
return False
# Index questions' types # Index questions' types
await load_questions() r = await load_questions()
if(not r and check_itemsQuestions):
logger.error("[CACHE] Questions were not loading correctly. Aborting filling cache...")
return False
# Clear cache data completely cache = {}
self.empty() orderList = []
success = True
p = 0 p = 0
try: try:
async with httpx.AsyncClient() as client: while 1:
while 1: p += 1
p += 1 res = await pretixClient.get(f"orders/?page={p}", expectedStatusCodes=[200, 404])
incPretixRead() if res.status_code == 404: break
res = await client.get(join(base_url_event, f"orders/?page={p}"), headers=headers) # Parse order data
if res.status_code == 404: break data = res.json()
# Parse order data for o in data['results']:
data = res.json() o = Order(o)
for o in data['results']: if o.status in ['canceled', 'expired']:
o = Order(o) self.remove_cache(o.code, cache=cache, orderList=orderList)
if o.status in ['canceled', 'expired']: else:
self.remove_cache(o.code) self.add_cache(Order(o), cache=cache, orderList=orderList)
else: self.lastCacheUpdate = time()
self.add_cache(Order(o)) logger.info(f"[CACHE] Cache filled in {self.lastCacheUpdate - start_time}s.")
self.lastCacheUpdate = time() except Exception:
logger.info(f"[CACHE] Cache filled in {self.lastCacheUpdate - start_time}s.") logger.error(f"[CACHE] Error while refreshing cache.\n{traceback.format_exc()}")
except Exception as ex: success = False
logger.error("[CACHE] Error while refreshing cache.", ex)
finally: finally:
self.updating = False self.updating.release()
# Apply new cache if there were no errors
if(success):
self.cache = cache
self.order_list = orderList
# Validating rooms # Validating rooms
rooms = list(filter(lambda o: (o.code == o.room_id), self.cache.values())) rooms = list(filter(lambda o: (o.code == o.room_id), self.cache.values()))
asyncio.create_task(validate_rooms(None, rooms, self)) asyncio.create_task(validate_rooms(None, rooms, self))
return success
async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False): async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False):
# if it's a nfc id, just retorn it # if it's a nfc id, just retorn it
@ -349,26 +368,24 @@ class OrderManager:
if re.match('^[A-Z0-9]{5}$', code or '') and (secret is None or re.match('^[a-z0-9]{16,}$', secret)): if re.match('^[A-Z0-9]{5}$', code or '') and (secret is None or re.match('^[a-z0-9]{16,}$', secret)):
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'Fetching {code} with secret {secret}') if DEV_MODE and EXTRA_PRINTS: logger.debug(f'Fetching {code} with secret {secret}')
async with httpx.AsyncClient() as client: res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None)
incPretixRead() if res.status_code != 200:
res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers) if request:
if res.status_code != 200: raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.")
if request:
raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.")
else:
self.remove_cache(code)
return None
res = res.json()
order = Order(res)
if order.status in ['canceled', 'expired']:
self.remove_cache(order.code)
if request:
raise exceptions.Forbidden(f"Your order has been deleted. Contact support with your order identifier ({res['code']}) for further info.")
else: else:
self.add_cache(order) self.remove_cache(code)
return None
if request and secret != res['secret']: res = res.json()
raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!")
return order order = Order(res)
if order.status in ['canceled', 'expired']:
self.remove_cache(order.code)
if request:
raise exceptions.Forbidden(f"Your order has been deleted. Contact support with your order identifier ({res['code']}) for further info.")
else:
self.add_cache(order)
if request and secret != res['secret']:
raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!")
return order

View File

@ -1,30 +1,42 @@
ROOM_ERROR_TYPES = { ROOM_ERROR_TYPES = {
'room_id_mismatch': "There's a member in your room that is actually in another room, too. Please contact us as soon as possible in order to fix this issue.", 'room_id_mismatch': "There's a member in your room that is actually in another room, too. Please contact us as soon as possible in order to fix this issue.",
'unpaid': "Somebody in your room has not paid for their reservation, yet.", 'unpaid': "Somebody in your room has not paid for their reservation, yet.",
'type_mismatch': "A member in your room has a ticket for a different type of room capacity. This happens when users swap their room types with others, without abandoning the room.", 'type_mismatch': "A member in your room has a ticket for a different type of room capacity. This happens when users swap their room types with others, without abandoning the room.",
'daily': "Some member in your room has a Daily ticket. These tickets do not include a hotel reservation.", 'daily': "Some member in your room has a Daily ticket. These tickets do not include a hotel reservation.",
'capacity_mismatch': "The number of people in your room mismatches your type of ticket." 'capacity_mismatch': "The number of people in your room mismatches your type of ticket."
} }
ROOM_UNCONFIRM_TITLE = "Your room got unconfirmed" EMAILS_TEXT = {
ROOM_UNCONFIRM_TEXT = { "ROOM_UNCONFIRM_TITLE": "Your room got unconfirmed",
'html': "Hello <b>{0}</b><br>We had to <b>unconfirm</b> your room <i>'{1}'</i> due to the following issues:<br></p>{2}<br><p>Please contact your room's owner or contact our support for further informations at <a href=\"https://furizon.net/contact/\"> https://furizon.net/contact/</a>.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>", "ROOM_UNCONFIRM_TEXT": {
'plain': "Hello {0}\nWe had to unconfirm your room '{1}' due to the following issues:\n{2}\nPlease contact your room's owner or contact our support for further informations at https://furizon.net/contact/.\nThank you\n\nTo manage your booking: https://reg.furizon.net/manage/welcome" 'html': "Hello <b>{0}</b><br>We had to <b>unconfirm</b> your room <i>'{1}'</i> due to the following issues:<br></p>{2}<br><p>Please contact your room's owner or contact our support for further informations at <a href=\"https://furizon.net/contact/\"> https://furizon.net/contact/</a>.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>",
'plain': "Hello {0}\nWe had to unconfirm your room '{1}' due to the following issues:\n{2}\nPlease contact your room's owner or contact our support for further informations at https://furizon.net/contact/.\nThank you\n\nTo manage your booking: https://reg.furizon.net/manage/welcome"
},
"MISSING_PROPIC_TITLE": "You haven't uploaded your badges yet!",
"MISSING_PROPIC_TEXT": {
'html': "Hello <b>{0}</b><br>We noticed you still have to <b>upload {1}</b>!<br>Please enter your booking page at <a href=\"https://reg.furizon.net/manage/welcome\"> https://reg.furizon.net/manage/welcome</a> and <b>upload them</b> under the <i>\"Badge Customization\"</i> section.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>",
'plain': "Hello {0}\nWe noticed you still have to upload {1}!\nPlease enter your booking page at https://reg.furizon.net/manage/welcome and upload them under the \"Badge Customization\" section.\nThank you."
},
} }
NOSECOUNT = { NOSECOUNT = {
'filters': { 'filters': {
'capacity': "Here are some furs that share your room type and don't have a confirmed room." 'capacity': "Here are some furs that share your room type and don't have a confirmed room."
} }
} }
LOCALES = { LOCALES = {
'shuttle_link': { 'shuttle_link': {
'en': 'Book now', 'en': 'Book now',
'it': 'Prenota ora' 'it': 'Prenota ora'
}, },
'shuttle_link_url': { 'shuttle_link_url': {
'en': 'https://visitfiemme.regiondo.com/furizon?_ga=2.129644046.307369854.1705325023-235291123.1705325023', 'en': 'https://visitfiemme.regiondo.com/furizon?_ga=2.129644046.307369854.1705325023-235291123.1705325023',
'it': 'https://experience.visitfiemme.it/furizon' 'it': 'https://experience.visitfiemme.it/furizon'
} }
} }

View File

@ -3,8 +3,10 @@ from logging import LogRecord
from config import * from config import *
METRICS_REQ_NO = 0 METRICS_REQ_NO = 0
METRICS_ERR_NO = 0 # Errors served to the clients
METRICS_PRETIX_READ = 0 METRICS_PRETIX_READ = 0
METRICS_PRETIX_WRITE = 0 METRICS_PRETIX_WRITE = 0
METRICS_PRETIX_ERRORS = 0 # Errors requesting pretix's backend
def incPretixRead(): def incPretixRead():
global METRICS_PRETIX_READ global METRICS_PRETIX_READ
@ -14,19 +16,32 @@ def incPretixWrite():
global METRICS_PRETIX_WRITE global METRICS_PRETIX_WRITE
METRICS_PRETIX_WRITE += 1 METRICS_PRETIX_WRITE += 1
def incPretixErrors():
global METRICS_PRETIX_ERRORS
METRICS_PRETIX_ERRORS += 1
def incReqNo(): def incReqNo():
global METRICS_REQ_NO global METRICS_REQ_NO
METRICS_REQ_NO += 1 METRICS_REQ_NO += 1
def incErrorNo(): # Errors served to the clients
global METRICS_ERR_NO
METRICS_ERR_NO += 1
def getMetricsText(): def getMetricsText():
global METRICS_REQ_NO global METRICS_REQ_NO
global METRICS_ERR_NO
global METRICS_PRETIX_READ global METRICS_PRETIX_READ
global METRICS_PRETIX_WRITE global METRICS_PRETIX_WRITE
global METRICS_PRETIX_ERRORS
out = [] out = []
out.append(f'sanic_request_count{{}} {METRICS_REQ_NO}') out.append(f'sanic_request_count{{}} {METRICS_REQ_NO}')
out.append(f'sanic_error_count{{}} {METRICS_ERR_NO}')
out.append(f'webint_pretix_read_count{{}} {METRICS_PRETIX_READ}') out.append(f'webint_pretix_read_count{{}} {METRICS_PRETIX_READ}')
out.append(f'webint_pretix_write_count{{}} {METRICS_PRETIX_WRITE}') out.append(f'webint_pretix_write_count{{}} {METRICS_PRETIX_WRITE}')
out.append(f'webint_pretix_error_count{{}} {METRICS_PRETIX_ERRORS}')
return "\n".join(out) return "\n".join(out)
@ -35,17 +50,29 @@ def getRoomCountersText(request):
try : try :
daily = 0 daily = 0
counters = {} counters = {}
counters_early = {}
counters_late = {}
for id in ROOM_TYPE_NAMES.keys(): for id in ROOM_TYPE_NAMES.keys():
counters[id] = 0 counters[id] = 0
counters_early[id] = 0
counters_late[id] = 0
for order in request.app.ctx.om.cache.values(): for order in request.app.ctx.om.cache.values():
if(order.daily): if(order.daily):
daily += 1 daily += 1
else: else:
counters[order.bed_in_room] += 1 counters[order.bed_in_room] += 1
if(order.has_early):
counters_early[order.bed_in_room] += 1
if(order.has_late):
counters_late[order.bed_in_room] += 1
for id, count in counters.items(): for id, count in counters.items():
out.append(f'webint_order_room_counter{{label="{ROOM_TYPE_NAMES[id]}"}} {count}') out.append(f'webint_order_room_counter{{days="normal", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
for id, count in counters_early.items():
out.append(f'webint_order_room_counter{{days="early", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
for id, count in counters_late.items():
out.append(f'webint_order_room_counter{{days="late", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
out.append(f'webint_order_room_counter{{label="Daily"}} {daily}') out.append(f'webint_order_room_counter{{label="Daily"}} {daily}')
except Exception as e: except Exception as e:

49
pretixClient.py Normal file
View File

@ -0,0 +1,49 @@
import httpx
from utils import *
from config import *
from sanic.log import logger
from metrics import *
import traceback
import asyncio
async def get(url, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
async def func(client : httpx.AsyncClient) -> httpx.Request:
return await client.get(join(baseUrl, url), headers=headers)
return await doReq(url, func, incPretixRead, expectedStatusCodes, "GETing")
async def post(url, content=None, json=None, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
async def func(client : httpx.AsyncClient) -> httpx.Request:
return await client.post(join(baseUrl, url), headers=headers, content=content, json=json)
return await doReq(url, func, incPretixWrite, expectedStatusCodes, "POSTing")
async def patch(url, json, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
async def func(client : httpx.AsyncClient) -> httpx.Request:
return await client.patch(join(baseUrl, url), headers=headers, json=json)
return await doReq(url, func, incPretixWrite, expectedStatusCodes, "PATCHing")
async def doReq(url, httpxFunc, metricsFunc, expectedStatusCodes, opLogString) -> httpx.Response:
res = None
async with httpx.AsyncClient(timeout=PRETIX_REQUESTS_TIMEOUT) as client:
requests = 0
for requests in range(PRETIX_REQUESTS_MAX):
try:
metricsFunc()
res = await httpxFunc(client)
if expectedStatusCodes is not None and res.status_code not in expectedStatusCodes:
incPretixErrors()
logger.warning(f"[PRETIX] Got an unexpected status code ({res.status_code}) while {opLogString} '{url}'. Allowed status codes: {', '.join(expectedStatusCodes)}")
continue
break
except Exception as e:
incPretixErrors()
logger.warning(f"[PRETIX] An error ({requests}) occurred while {opLogString} '{url}':\n{traceback.format_exc()}")
requests += 1
else:
logger.error(f"[PRETIX] Reached PRETIX_REQUESTS_MAX ({PRETIX_REQUESTS_MAX}) while {opLogString} '{url}'. Aborting")
raise httpx.TimeoutException(f"PRETIX_REQUESTS_MAX reached while {opLogString} to pretix.")
return res

View File

@ -12,6 +12,21 @@ summary:has(span.status) {
background-color: #ffaf0377; background-color: #ffaf0377;
} }
.rainbow-text {
background-image: repeating-linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff);
background-size: 2000% 2000%;
color: transparent;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: rainbow 4s linear infinite;
}
@keyframes rainbow {
0% { background-position:0% 0%; }
100% { background-position:57.75% 0%; }
}
/* Dark theme */ /* Dark theme */
@media only screen and (prefers-color-scheme: dark) { @media only screen and (prefers-color-scheme: dark) {
.icon {filter: invert(1);} .icon {filter: invert(1);}

View File

@ -14,6 +14,7 @@ nav#topbar {
top: 0rem; top: 0rem;
transition: top 300ms; transition: top 300ms;
line-height: 2em; line-height: 2em;
max-width:98vw;
} }
nav#topbar a { nav#topbar a {
@ -42,21 +43,6 @@ nav img {
box-sizing: border-box; box-sizing: border-box;
} }
a.rainbow-text {
background-image: repeating-linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff);
background-size: 2000% 2000%;
color: transparent;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: rainbow 4s linear infinite;
}
@keyframes rainbow {
0% { background-position:0% 0%; }
100% { background-position:57.75% 0%; }
}
nav a#mobileMenu { nav a#mobileMenu {
display: none; display: none;
} }

15
room.py
View File

@ -335,21 +335,6 @@ async def confirm_room(request, order: Order, quotas: Quotas):
await rm.edit_answer('pending_roommates', None) await rm.edit_answer('pending_roommates', None)
await rm.edit_answer('pending_room', None) await rm.edit_answer('pending_room', None)
# This should now be useless because in the ticket there already is the ticket/room type
# thing = {
# 'order': order.code,
# 'addon_to': order.position_positionid,
# 'item': ITEM_IDS['room'],
# 'variation': ROOM_MAP[len(room_members)]
# }
#
# async with httpx.AsyncClient() as client:
# incPretixRead()
# res = await client.post(join(base_url_event, "orderpositions/"), headers=headers, json=thing)
#
# if res.status_code != 201:
# raise exceptions.BadRequest("Something has gone wrong! Please contact support immediately")
for rm in room_members: for rm in room_members:
await rm.send_answers() await rm.send_answers()

7
stuff/rclone/clone.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
/usr/bin/tar -czvf /tmp/backupRclone.tar.gz /home/ /root/ /etc/ /var/backups/ /var/log/ /var/mail/ /var/pretix-data/ /var/prometheus-data/ /var/spool/ /var/www/ /var/lib/grafana/ /var/lib/redis/
/usr/bin/rclone sync /tmp/backupRclone.tar.gz webservice:/backups/backupRclone.tar.gz -P --stats=1s --bwlimit 15M
/usr/bin/rm /tmp/backupRclone.tar.gz

View File

@ -0,0 +1,8 @@
#!/bin/bash
source /root/.profile
/usr/bin/tar -czf /tmp/backupRclone.tar.gz /home/ /root/ /etc/ /var/backups/ /var/log/ /var/mail/ /var/pretix-data/ /var/prometheus-data/ /var/spool/ /var/www/ /var/lib/grafana/ /var/lib/redis/
/usr/bin/rclone sync /tmp/backupRclone.tar.gz webservice:/backups/backupRclone.tar.gz --bwlimit 15M
/usr/bin/rm /tmp/backupRclone.tar.gz

View File

@ -9,10 +9,11 @@
</picture> </picture>
</header> </header>
<!-- Quick controls --> <!-- Quick controls -->
<h2>Admin panel</h2> <h2>Admin panel</h2>
<a href="/manage/admin/cache/clear" role="button" title="Reload the orders' data and the re-sync items' indexes from pretix">Clear cache</a> <a href="/manage/admin/cache/clear" role="button" title="Reload the orders' data and the re-sync items' indexes from pretix">Clear cache</a>
<a href="/manage/nosecount" role="button" title="Shortcut to the nosecount's admin data">Manage rooms</a> <a href="/manage/nosecount" role="button" title="Shortcut to the nosecount's admin data">Manage rooms</a>
<a href="/manage/admin/room/verify" role="button" title="Will unconfirm rooms that fail the default check. Useful when editing answers from Pretix">Verify Rooms</a> <a href="/manage/admin/room/verify" role="button" title="Will unconfirm rooms that fail the default check. Useful when editing answers from Pretix">Verify Rooms</a>
<a href="/manage/admin/propic/remind" role="button" title="Will remind via mail all people who event uploaded a badge to do it">Remind badge upload</a>
<hr> <hr>
</main> </main>

View File

@ -5,7 +5,7 @@
<p class="notice" style="background:#0881c0"><b>Note! </b> Only people with the same room type can be roommates. If you need help, contact the <a href="https://furizon.net/contact/">Furizon's Staff</a>.</p> <p class="notice" style="background:#0881c0"><b>Note! </b> Only people with the same room type can be roommates. If you need help, contact the <a href="https://furizon.net/contact/">Furizon's Staff</a>.</p>
{% if not order.room_confirmed %} {% if not order.room_confirmed %}
<p class="notice" style="background:#0881c0"><b><a href="https://furizon.net/manage/nosecount?filter=capacity">Check here</a> for any fur who share your room type.</p> <p class="notice" style="background:#0881c0"><b><a href="/manage/nosecount?filter=capacity">Check here</a> for any fur who share your room type.</p>
{% endif %} {% endif %}
{# Show alert if room owner has wrong people inside #} {# Show alert if room owner has wrong people inside #}

View File

@ -1,10 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Error {{exception.status_code}}{% endblock %} {% block title %}Error {{status_code}}{% endblock %}
{% block main %} {% block main %}
<main class="container"> <main class="container">
<h1>{{exception.status_code}}</h1> <h1>{{status_code}}</h1>
<p>{{exception}}</p> <p>{{exception}}</p>
{% if exception.status_code == 409 %} {% if status_code == 409 %}
<p>Retrying in 1 second...</p> <p>Retrying in 1 second...</p>
<meta http-equiv="refresh" content="1"> <meta http-equiv="refresh" content="1">
{% endif %} {% endif %}

View File

@ -7,6 +7,8 @@ from email_util import send_unconfirm_message
from sanic.response import text, html, redirect, raw from sanic.response import text, html, redirect, raw
from sanic.log import logger from sanic.log import logger
from metrics import * from metrics import *
import pretixClient
import traceback
METADATA_TAG = "meta_data" METADATA_TAG = "meta_data"
VARIATIONS_TAG = "variations" VARIATIONS_TAG = "variations"
@ -28,34 +30,38 @@ QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.htm
TYPE_OF_QUESTIONS = {} # maps questionId -> type TYPE_OF_QUESTIONS = {} # maps questionId -> type
async def load_questions(): async def load_questions() -> bool:
global TYPE_OF_QUESTIONS global TYPE_OF_QUESTIONS
TYPE_OF_QUESTIONS.clear() # TYPE_OF_QUESTIONS.clear() It should not be needed
async with httpx.AsyncClient() as client: logger.info("[QUESTIONS] Loading questions...")
success = True
try:
p = 0 p = 0
while 1: while 1:
p += 1 p += 1
incPretixRead() res = await pretixClient.get(f"questions/?page={p}", expectedStatusCodes=[200, 404])
res = await client.get(join(base_url_event, f"questions/?page={p}"), headers=headers)
if res.status_code == 404: break if res.status_code == 404: break
data = res.json() data = res.json()
for q in data['results']: for q in data['results']:
TYPE_OF_QUESTIONS[q['id']] = q['type'] TYPE_OF_QUESTIONS[q['id']] = q['type']
except Exception:
logger.warning(f"[QUESTIONS] Error while loading questions.\n{traceback.format_exc()}")
success = False
return success
async def load_items(): async def load_items() -> bool:
global ITEMS_ID_MAP global ITEMS_ID_MAP
global ITEM_VARIATIONS_MAP global ITEM_VARIATIONS_MAP
global CATEGORIES_LIST_MAP global CATEGORIES_LIST_MAP
global ROOM_TYPE_NAMES global ROOM_TYPE_NAMES
async with httpx.AsyncClient() as client: logger.info("[ITEMS] Loading items...")
success = True
try:
p = 0 p = 0
while 1: while 1:
p += 1 p += 1
incPretixRead() res = await pretixClient.get(f"items/?page={p}", expectedStatusCodes=[200, 404])
res = await client.get(join(base_url_event, f"items/?page={p}"), headers=headers)
if res.status_code == 404: break if res.status_code == 404: break
data = res.json() data = res.json()
@ -85,6 +91,10 @@ async def load_items():
logger.debug(f'Mapped Variations: %s', ITEM_VARIATIONS_MAP) logger.debug(f'Mapped Variations: %s', ITEM_VARIATIONS_MAP)
logger.debug(f'Mapped categories: %s', CATEGORIES_LIST_MAP) logger.debug(f'Mapped categories: %s', CATEGORIES_LIST_MAP)
logger.debug(f'Mapped Rooms: %s', ROOM_TYPE_NAMES) logger.debug(f'Mapped Rooms: %s', ROOM_TYPE_NAMES)
except Exception:
logger.warning(f"[ITEMS] Error while loading items.\n{traceback.format_exc()}")
success = False
return success
# Tries to get an item name from metadata. Prints a warning if an item has no metadata # Tries to get an item name from metadata. Prints a warning if an item has no metadata
def check_and_get_name(type, q): def check_and_get_name(type, q):
@ -141,7 +151,7 @@ async def get_people_in_room_by_code(request, code, om=None):
await om.update_cache() await om.update_cache()
return filter(lambda rm: rm.room_id == code, om.cache.values()) return filter(lambda rm: rm.room_id == code, om.cache.values())
async def unconfirm_room_by_order(order, room_members:[]=None, throw=True, request=None, om=None): async def unconfirm_room_by_order(order, room_members=None, throw=True, request=None, om=None):
if not om: om = request.app.ctx.om if not om: om = request.app.ctx.om
if not order.room_confirmed: if not order.room_confirmed:
if throw: if throw:
@ -206,7 +216,7 @@ async def validate_rooms(request, rooms, om):
order = rtu[0] order = rtu[0]
member_orders = rtu[2] member_orders = rtu[2]
try: try:
await send_unconfirm_message (order, member_orders) await send_unconfirm_message(order, member_orders)
sent_count += len(member_orders) sent_count += len(member_orders)
except Exception as ex: except Exception as ex:
if EXTRA_PRINTS: logger.exception(str(ex)) if EXTRA_PRINTS: logger.exception(str(ex))