Added NFC-related functions
This commit is contained in:
parent
3c4d31e1b9
commit
5ca739cec2
|
@ -0,0 +1,53 @@
|
|||
from sanic import Blueprint, exceptions, response
|
||||
from time import time
|
||||
from asyncio import Future
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
bp = Blueprint("money", url_prefix="/money")
|
||||
|
||||
@bp.post("/pos")
|
||||
async def do_transaction(request):
|
||||
|
||||
message = ''
|
||||
tx_id = request.app.ctx.money.execute('INSERT INTO tx(tag_id, amount, ts) VALUES (?,?,?) RETURNING id', (request.form.get('nfc_id'), request.form.get('total'), time())).fetchone()[0]
|
||||
|
||||
for item, qty in request.form.items():
|
||||
if not item.startswith('itm_'): continue
|
||||
if qty[0] == '0': continue
|
||||
request.app.ctx.money.execute('INSERT INTO tx_items(tx_id, item_id, qty) VALUES (?,?,?)', (tx_id, item[4:], qty[0]))
|
||||
|
||||
request.app.ctx.money.commit()
|
||||
return await show_transactions(request, message='Transazione eseguita con successo!')
|
||||
|
||||
@bp.get("/pos")
|
||||
async def show_transactions(request, message=None):
|
||||
tpl = request.app.ctx.tpl.get_template('pos.html')
|
||||
items = request.app.ctx.money.execute('SELECT * FROM item')
|
||||
|
||||
tx_info = {}
|
||||
last_tx = request.app.ctx.money.execute('SELECT * FROM tx WHERE amount < 0 ORDER BY ts DESC LIMIT 3').fetchall()
|
||||
for tx in last_tx:
|
||||
tx_info[tx['id']] = {'items': request.app.ctx.money.execute('SELECT * FROM tx_items JOIN item ON item_id = item.id AND tx_id = ?', (tx['id'],)).fetchall(), 'order': await request.app.ctx.om.get_order(nfc_id=tx['tag_id']), 'time': datetime.fromtimestamp(tx['ts'] or 0).strftime('%H:%M')}
|
||||
|
||||
return response.html(tpl.render(items=items, message=message, last_tx=last_tx, tx_info=tx_info))
|
||||
|
||||
@bp.get("/poll_barcode")
|
||||
async def give_barcode(request):
|
||||
|
||||
request.app.ctx.nfc_reads[request.ip] = Future()
|
||||
|
||||
try:
|
||||
bcd = await asyncio.wait_for(request.app.ctx.nfc_reads[request.ip], 20)
|
||||
except asyncio.TimeoutError:
|
||||
if not request.ip in request.app.ctx.nfc_reads:
|
||||
del request.app.ctx.nfc_reads[request.ip]
|
||||
return response.json({'error': 'no_barcode'})
|
||||
|
||||
info = request.app.ctx.money.execute("SELECT count(*), coalesce(sum(amount), 0) FROM tx WHERE coalesce(is_canceled, 0) != 1 AND tag_id = ?", (bcd['id'],)).fetchone()
|
||||
|
||||
order = await request.app.ctx.om.get_order(nfc_id=bcd['id'])
|
||||
|
||||
desc = ("⚠️" if not bcd['is_secure'] else '') + (f"👤 {order.code} {order.name}" if order else f"🪙 {bcd['id']}") + f" · Transazioni: {info[0]}"
|
||||
|
||||
return response.json({**bcd, 'txamt': info[0], 'balance': info[1], 'desc': desc})
|
|
@ -0,0 +1,72 @@
|
|||
from sanic import Blueprint, exceptions, response
|
||||
from random import choice
|
||||
from ext import *
|
||||
from config import headers, PROPIC_DEADLINE
|
||||
from PIL import Image
|
||||
from os.path import isfile
|
||||
from os import unlink
|
||||
from io import BytesIO
|
||||
from hashlib import sha224
|
||||
from time import time
|
||||
from urllib.parse import unquote
|
||||
from base64 import b16decode
|
||||
from asyncio import Future
|
||||
import json
|
||||
import re
|
||||
import asyncio
|
||||
|
||||
bp = Blueprint("nfc")
|
||||
|
||||
@bp.post("/nfc/read")
|
||||
async def handle_reading(request):
|
||||
payload = request.json
|
||||
if payload['count']:
|
||||
request.app.ctx.nfc_counts.execute('INSERT INTO read_count(tag_id, ts, count) VALUES (?,?,?)', (payload['id'], int(time()), payload['count']))
|
||||
request.app.ctx.nfc_counts.commit()
|
||||
|
||||
if payload['boopbox_id']:
|
||||
await request.app.ctx.boopbox_queue.put(payload)
|
||||
return response.text('ok')
|
||||
|
||||
if not request.ip in request.app.ctx.nfc_reads:
|
||||
return response.text('wasted read')
|
||||
|
||||
try:
|
||||
request.app.ctx.nfc_reads[request.ip].set_result(payload)
|
||||
except asyncio.exceptions.InvalidStateError:
|
||||
del request.app.ctx.nfc_reads[request.ip]
|
||||
|
||||
return response.text('ok')
|
||||
|
||||
@bp.get("/fz23/<tag_id:([bc][0-9A-F]{14}x[0-9A-F]{6})>")
|
||||
async def handle_nfc_tag(request, tag_id):
|
||||
|
||||
tag = re.match('([bc])([0-9A-F]{14})x([0-9A-F]{6})', tag_id)
|
||||
|
||||
# Store the read count
|
||||
read_count = int.from_bytes(b16decode(tag.group(3)))
|
||||
request.app.ctx.nfc_counts.execute('INSERT INTO read_count(tag_id, ts, count, is_web) VALUES (?,?,?,1)', (tag.group(2), int(time()), read_count))
|
||||
request.app.ctx.nfc_counts.commit()
|
||||
|
||||
# If it's a coin, just show the coin template
|
||||
if tag.group(1) == 'c':
|
||||
return response.redirect(f"coin/{tag.group(2)}")
|
||||
|
||||
# If it's a badge, look for the corresponding order
|
||||
for o in request.app.ctx.om.cache.values():
|
||||
if o.nfc_id == tag.group(2):
|
||||
return response.redirect(o.code)
|
||||
|
||||
raise exceptions.NotFound("Unknown tag :(")
|
||||
|
||||
@bp.get("/fz23/coin/<tag_id>")
|
||||
async def show_coin(request, tag_id):
|
||||
balance = request.app.ctx.money.execute('SELECT sum(amount) FROM tx WHERE tag_id = ?', (tag_id,)).fetchone()[0]
|
||||
|
||||
tpl = request.app.ctx.tpl.get_template('coin.html')
|
||||
return response.html(tpl.render(balance=balance))
|
||||
|
||||
@bp.get("/fz23/<code:[A-Z0-9]{5}>")
|
||||
async def show_order(request, code):
|
||||
return response.html(f"<h1>Badge</h1><p>{code}</p>")
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
|
||||
<meta charset="utf8" />
|
||||
<title>{{order.name}}</title>
|
||||
<meta name="viewport" content="width=380rem" />
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Lato&display=swap');
|
||||
* {border:0;margin:0;padding:0;}
|
||||
|
||||
body{background:#000;color:#ddd;font-family:'Lato';font-size:1.1rem;}
|
||||
h1 {font-size: 3.5em;text-transform:uppercase}
|
||||
#center {margin:1em auto;padding:0 1rem;max-width:30rem;text-align:center;}
|
||||
nav > a {font-size:1.1em;display:block;background-color:#336;color:#fff;text-decoration:none;padding:0.5em;border-radius:3em;text-align:center;margin:1em;font-weight:bold;}
|
||||
a > img {display:inline;height:1em;line-height:1em;vertical-align:middle;}
|
||||
p {margin:0.6em 0;}
|
||||
hr {border:0;height:12px;background:url('wiggly.png');background-repeat:no-repeat;background-position:center;margin:1.5em 0;}
|
||||
a {color:#f0f;}
|
||||
|
||||
.bebas {font-family:'Bebas Neue';font-weight:900;background: linear-gradient(to right, #f32170, #ff6b08, #cf23cf, #eedd44);
|
||||
-webkit-text-fill-color: transparent;-webkit-background-clip: text;transform:.3s all;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="center">
|
||||
<img src="https://reg.furizon.net/res/propic/{{order.propic or 'default.png'}}" style="width:100%;max-height:10em;max-width:10em;"/>
|
||||
<hr />
|
||||
<h2 class="bebas" style="font-size:4em;font-family:'Bebas Neue';">{{order.name}}</h2>
|
||||
<p>Is an attendee of Furizon 2023 :D<br/><br />
|
||||
<hr />
|
||||
<nav>
|
||||
<a style="background:#1086D7;" href="https://t.me/{{order.telegram_username}}">Telegram</a>
|
||||
</nav>
|
||||
<h3 class="bebas">Stay cool~<br/>Stay Furizon</h3>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>You found a coin!</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>
|
||||
@import url(https://fonts.bunny.net/css?family=arbutus:400);
|
||||
body {font-family:'arbutus', 'Comic Sans MS', sans-serif;background:url('/res/wood.jpg') #222 no-repeat;background-size:cover;color:#eee;text-align:center;}
|
||||
|
||||
strong {color:#de0;}
|
||||
main {display:block;max-width:26em;margin:1em auto;}
|
||||
video {display:block;width:100%;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<video autoplay muted oncanplay="preloadSecondVideo()" onended="loopVideo()">
|
||||
<source src="/res/video/coin-intro.webm" type="video/webm">
|
||||
<img src="/res/video/coin.webp" alt="Fallback Image">
|
||||
</video>
|
||||
{% if balance < 0 %}
|
||||
<h1>You found a<br/><strong>{{balance}}FZ</strong><br />coin!</h1>
|
||||
{% else %}
|
||||
<h1>You found a coin!<br />(but sadly, it's already spent)</h1>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function preloadSecondVideo() {
|
||||
var secondVideo = new Image(); // Create an image element to preload the second video
|
||||
secondVideo.src = '/res/video/coin-loop.webm';
|
||||
}
|
||||
|
||||
function loopVideo() {
|
||||
var video = document.querySelector('video');
|
||||
video.src = '/res/video/coin-loop.webm';
|
||||
video.loop = true;
|
||||
video.load();
|
||||
video.play();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,294 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Fz vending</title>
|
||||
<style>
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25%, 75% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
body {margin:0;padding:0;background:#222;color:#eee;font-family:"Open Sans", monospace;}
|
||||
|
||||
header {padding-left:0.5em;height:4rem;line-height:4rem;background:#111;font-size:2.2rem;color:#fa0;}
|
||||
header img {height:3.6rem;margin-right:0.3em;display:inline-block;vertical-align:middle;}
|
||||
header strong {font-size:1.05em;color:#f00;vertical-align:baseline;}
|
||||
aside {position:fixed;height:100%;overflow:auto;top:0;width:18em;right:0;background:#111;}
|
||||
|
||||
main {padding-right:18em;}
|
||||
|
||||
.shake {animation: shake 0.3s 1;}
|
||||
|
||||
#toolkit {position:fixed;bottom:0;right:0;width:18em;padding:0.5em;box-sizing:border-box;background:#000;text-align:center;}
|
||||
#toolkit img {display:inline;height:3rem;margin:0.5rem 0.1rem;border:1px dashed rgba(255,255,255,0.2);padding:0.2em;}
|
||||
|
||||
h2 {margin-top:0;font-size: 2.2rem;line-height:4rem;display:block;text-align:center;background:#070;color:#eee;}
|
||||
h3 {margin-top:1em;font-size: 1.4rem;display:block;text-align:center;padding:0.3em 0em;background:#fa0;color:#000;}
|
||||
|
||||
button {background: #fa0;padding:0.2em;margin:0.1em;font-size:1.2em;width:1.6em;border-radius:6px;border:none;transition:all 0.1s;}
|
||||
button:active {transform: scale(0.95);}
|
||||
|
||||
aside strong {font-size:1.9em;color:#f00;vertical-align:baseline;}
|
||||
aside button {font-size:3em;}
|
||||
|
||||
ul {padding:0.5em 1em;}
|
||||
li {line-height:1.8em;}
|
||||
p {margin-bottom: 1em;}
|
||||
em {font-size: 1.3em;font-weight:bold;}
|
||||
sup {font-size: 0.7em;color:#fc0;}
|
||||
|
||||
table {width:100%;font-size:1.3em;}
|
||||
table td {padding:0.1em;}
|
||||
table td:first-child {color:#0f0;text-align:right;}
|
||||
|
||||
#mask {position:fixed;height:100%;width:100%;top:0;left:0;background:rgba(0,0,0,0.3);z-index:9999;backdrop-filter: blur(20px);text-align:center;}
|
||||
#mask h1 {font-size:4em;margin:3em 0 0.6em;}
|
||||
#mask button {font-size:3em;background:#ddd;}
|
||||
|
||||
.secret {display:none;}
|
||||
|
||||
input {color:#eee;background:#000;font-size:2em;width:2em;font-size:center;border:none;text-align:center;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header id="nfcInfo">{{message or 'Scannerizza un NFC...'}}</header>
|
||||
<form method="post" action="pos">
|
||||
<input type="hidden" id="totalValue" name="total" />
|
||||
<input type="hidden" id="nfcid" name="nfc_id" value="" />
|
||||
<main>
|
||||
<table>
|
||||
{% for item in items %}
|
||||
<tr {% if item.price > 0 %}class="secret"{% endif %}>
|
||||
<td>{{item.name}}</td>
|
||||
<td><strong>{{item.price}}FZ</strong></td>
|
||||
<td>
|
||||
<button type="button" onclick="decrementValue(this)">-</button>
|
||||
<input type="text" name="itm_{{item.id}}" value="0" data-price="{{item.price}}" autocomplete="off" oninput="updateTotal();" />
|
||||
<button type="button" onclick="incrementValue(this)">+</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</main>
|
||||
<aside>
|
||||
<h2><span id="badgeBalance">0</span> FZ</h2>
|
||||
<p style="text-align:center;">Totale <strong><span id="totalValueShow">0</span>FZ</strong></p>
|
||||
<div style="text-align:center;"><button style="background:#090;" id="sendButton" disabled="disabled">✓</button><button type="button" onClick="zeroItems();" style="background:#c00;">⛌</button></div>
|
||||
<h3 id="triggerElement">Ultime transazioni</h3>
|
||||
{% for tx in last_tx %}
|
||||
<ul>
|
||||
<li><em>{{tx_info[tx['id']]['order'].name or tx['tag_id']}}<sup>{{tx_info[tx['id']]['time']}}</sup></em></li>
|
||||
{% for tx_item in tx_info[tx['id']]['items'] %}
|
||||
<li><strong>{{tx_item['qty']}}x</strong> {{tx_item['name']}}</li>
|
||||
{% endfor %}
|
||||
<li><em>Totale: {{tx['amount']}}FZ</em></li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
<span style="display:none;" id="debug"></span>
|
||||
</aside>
|
||||
<div id="toolkit">
|
||||
<a href="pos"><img src="/res/icons/refresh.svg" style="filter: invert(1);"/></a>
|
||||
<a href="#"><img src="/res/icons/qr.svg" id="executeButton" style="filter: invert(1);"/></a>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
let isClosing = false;
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
function handleBeforeUnload(event) {
|
||||
isClosing = true;
|
||||
}
|
||||
|
||||
const triggerElement = document.getElementById('triggerElement');
|
||||
const secretElements = document.querySelectorAll('.secret');
|
||||
let secretElementsVisible = localStorage.getItem('secretElementsVisible');
|
||||
|
||||
let pressStart;
|
||||
let isPress = false;
|
||||
|
||||
triggerElement.addEventListener('mousedown', handleMouseDown);
|
||||
triggerElement.addEventListener('touchstart', handleMouseDown);
|
||||
triggerElement.addEventListener('mouseup', handleMouseUp);
|
||||
triggerElement.addEventListener('touchend', handleMouseUp);
|
||||
|
||||
function handleMouseDown() {
|
||||
pressStart = Date.now();
|
||||
isPress = true;
|
||||
setTimeout(toggleSecretElements, 1200); // Delay the toggle action by 500 milliseconds
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
isPress = false;
|
||||
}
|
||||
|
||||
if (secretElementsVisible === 'true') {
|
||||
secretElements.forEach(element => {
|
||||
element.classList.remove('secret');
|
||||
});
|
||||
}
|
||||
|
||||
const executeButton = document.getElementById('executeButton');
|
||||
|
||||
executeButton.addEventListener('click', () => {
|
||||
const code = prompt('Enter the code:');
|
||||
if (code && /^([A-F0-9]{2}){4,}$/.test(code)) {
|
||||
const payload = {
|
||||
id: code,
|
||||
is_secure: false,
|
||||
count: null,
|
||||
boopbox_id: null
|
||||
};
|
||||
|
||||
fetch('/nfc/read', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Handle the response data as needed
|
||||
console.log(data);
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle any errors that occur during the POST request
|
||||
console.error(error);
|
||||
});
|
||||
} else if (code !== null) {
|
||||
alert('Invalid code format. Please enter a valid code.');
|
||||
}
|
||||
});
|
||||
|
||||
function toggleSecretElements() {
|
||||
const pressDuration = Date.now() - pressStart;
|
||||
if (pressDuration >= 1000 && isPress) {
|
||||
secretElementsVisible = !secretElementsVisible;
|
||||
|
||||
if (secretElementsVisible) {
|
||||
localStorage.setItem('secretElementsVisible', 'true');
|
||||
secretElements.forEach(element => {
|
||||
element.classList.remove('secret');
|
||||
});
|
||||
} else {
|
||||
localStorage.removeItem('secretElementsVisible');
|
||||
secretElements.forEach(element => {
|
||||
element.classList.add('secret');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to perform long-polling
|
||||
function longPoll() {
|
||||
fetch('poll_barcode')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
// Update the table with received JSON data
|
||||
|
||||
if(!data.error) {
|
||||
document.getElementById('nfcInfo').classList.remove('shake');
|
||||
document.getElementById('nfcInfo').innerHTML = data.desc;
|
||||
void document.getElementById('nfcInfo').offsetWidth;
|
||||
document.getElementById('nfcInfo').classList.add('shake');
|
||||
|
||||
document.getElementById('nfcid').value = data.id;
|
||||
document.getElementById('badgeBalance').innerHTML = data.balance;
|
||||
document.getElementById('debug').innerHTML = JSON.stringify(data);
|
||||
} else {
|
||||
console.log("Got no barcode.")
|
||||
}
|
||||
|
||||
// Start the next long-poll request
|
||||
updateTotal();
|
||||
longPoll();
|
||||
})
|
||||
.catch((error) => {
|
||||
if(isClosing) return;
|
||||
document.getElementById('nfcInfo').innerHTML = error;
|
||||
// Retry long-polling after a delay in case of errors
|
||||
setTimeout(longPoll, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Start long-polling
|
||||
longPoll();
|
||||
|
||||
|
||||
function incrementValue(button) {
|
||||
var input = button.previousElementSibling;
|
||||
var value = parseInt(input.value) || 0;
|
||||
input.value = Math.max(value + 1, 0);
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
function decrementValue(button) {
|
||||
var input = button.nextElementSibling;
|
||||
var value = parseInt(input.value) || 0;
|
||||
input.value = Math.max(value - 1, 0);
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
function zeroItems() {
|
||||
var inputs = document.querySelectorAll('input[name^="itm_"]');
|
||||
inputs.forEach(function(input) {
|
||||
input.value = 0;
|
||||
});
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
function updateTotal() {
|
||||
var inputs = document.querySelectorAll('input[name^="itm_"]');
|
||||
var total = 0;
|
||||
inputs.forEach(function(input) {
|
||||
var value = parseInt(input.value) || 0;
|
||||
var price = parseFloat(input.getAttribute('data-price'));
|
||||
total += value * price;
|
||||
});
|
||||
document.getElementById('totalValue').value = total;
|
||||
document.getElementById('totalValueShow').innerHTML = total;
|
||||
|
||||
let badgeBalance = Number(document.getElementById('badgeBalance').innerHTML);
|
||||
|
||||
if(badgeBalance+total < 0 || total == 0) {
|
||||
document.getElementById('sendButton').setAttribute("disabled", "disabled");
|
||||
} else {
|
||||
document.getElementById('sendButton').removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</html>
|
Loading…
Reference in New Issue