Compare commits
5 Commits
27e8518be3
...
5c35a8586c
Author | SHA1 | Date | |
---|---|---|---|
5c35a8586c | |||
5d99e78dea | |||
cd663087c8 | |||
07e444c3b4 | |||
214d3502c0 |
@ -1,13 +1,31 @@
|
||||
Adafruit-Thermal>=1.1.0
|
||||
Click>=7.0
|
||||
Flask>=1.0.2
|
||||
Flask-Limiter>=1.0.1
|
||||
itsdangerous>=0.24
|
||||
Jinja2>=2.10
|
||||
limits>=1.3
|
||||
MarkupSafe>=1.0
|
||||
Pillow>=5.3.0
|
||||
pyserial>=3.4
|
||||
six>=1.11.0
|
||||
Unidecode>=1.0.22
|
||||
Werkzeug>=0.14.1
|
||||
Adafruit-Thermal==1.1.0
|
||||
appdirs==1.4.4
|
||||
argcomplete==2.0.0
|
||||
click==8.1.3
|
||||
commonmark==0.9.1
|
||||
Deprecated==1.2.13
|
||||
escpos==1.9
|
||||
Flask==2.1.2
|
||||
Flask-Limiter==2.4.5.1
|
||||
future==0.18.2
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
limits==2.6.1
|
||||
MarkupSafe==2.1.1
|
||||
packaging==21.3
|
||||
Pillow==9.1.0
|
||||
Pygments==2.12.0
|
||||
pyparsing==3.0.8
|
||||
pyserial==3.5
|
||||
python-barcode==0.13.1
|
||||
pyusb==1.2.1
|
||||
PyYAML==6.0
|
||||
qrcode==7.3.1
|
||||
rich==12.4.1
|
||||
six==1.16.0
|
||||
toml==0.10.2
|
||||
typing_extensions==4.2.0
|
||||
Unidecode==1.3.4
|
||||
viivakoodi==0.8.0
|
||||
Werkzeug==2.1.2
|
||||
wrapt==1.14.1
|
||||
|
4
run.sh
4
run.sh
@ -1,3 +1,7 @@
|
||||
virtualenv .
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
export FLASK_APP=src/main.py
|
||||
export FLASK_ENV=development
|
||||
flask run --host 192.168.0.42 --debugger --eager-loading
|
||||
|
23
src/main.py
23
src/main.py
@ -122,17 +122,24 @@ def api_print_image():
|
||||
sign = request.form["signature"]
|
||||
except Exception as e:
|
||||
flash(str(e),'error')
|
||||
redirect(url_for('index'))
|
||||
app.logger.error(str(e) + " - Whoops, no forms submitted or missing signature.")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
|
||||
if 'img' not in request.files:
|
||||
flash('No file found. Did you use the good form ?', 'error')
|
||||
return redirect(url_for("index"))
|
||||
else:
|
||||
file = request.files['img']
|
||||
|
||||
try:
|
||||
if 'img' not in request.files:
|
||||
flash('No file found. Did you use the good form ?', 'error')
|
||||
app.logger.error("No file found. Did you use the good form ?")
|
||||
return redirect(url_for("index"))
|
||||
else:
|
||||
file = request.files['img']
|
||||
except Exception as e:
|
||||
if sign is not None and photo is not None:
|
||||
pass
|
||||
else:
|
||||
flash(str(e), 'error')
|
||||
app.logger.error("Couldn't get an image nor signature : " + str(e))
|
||||
|
||||
# If the user does not select a file, the browser submits an
|
||||
# empty file without a filename.
|
||||
|
160
src/static/js/webcam.js
Normal file
160
src/static/js/webcam.js
Normal file
@ -0,0 +1,160 @@
|
||||
let streaming;
|
||||
var width = document.getElementById("video").parentNode.parentElement.clientWidth;
|
||||
var height = width / (4 / 3);
|
||||
|
||||
function startup(){
|
||||
video = document.getElementById('video');
|
||||
canvas = document.getElementById('canvas');
|
||||
photo = document.getElementById('photo');
|
||||
switch_cameras = document.getElementById('flip')
|
||||
printButton = document.getElementById('print_button');
|
||||
|
||||
if ( check_webcam() ){
|
||||
setup_events();
|
||||
clear_canvas();
|
||||
} else {
|
||||
console.log("Seems like it's impossible to get a webcam.");
|
||||
}
|
||||
}
|
||||
|
||||
function check_webcam(){
|
||||
try {
|
||||
// access video stream from webcam
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false
|
||||
})
|
||||
// on success, stream it in video tag
|
||||
// the video tag is hidden, as is the canvas.
|
||||
.then(function(stream) {
|
||||
switch_cameras.removeAttribute("disabled","");
|
||||
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("An error occurred: " + err);
|
||||
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("An error occurred: " + err);
|
||||
console.log("Seems like they is no webcam available.")
|
||||
|
||||
// We disable the print button is it cannot be clicked.
|
||||
printButton.setAttribute("disabled","");
|
||||
|
||||
// We create an alert message.
|
||||
const frame_div = document.getElementById("image_dither");
|
||||
frame_div.removeAttribute('class');
|
||||
|
||||
const alert_div = document.createElement("div");
|
||||
alert_div.setAttribute("class", "alert alert-warning");
|
||||
alert_div.setAttribute("role", "alert");
|
||||
|
||||
var alert_message = document.createTextNode("We where unable to get a Webcam device, this page will not work.");
|
||||
alert_div.appendChild(alert_message);
|
||||
frame_div.appendChild(alert_div);
|
||||
console.log("Should be a new div somewhere");
|
||||
throw new Error("Unable to get a video device, stopping the photobooth.");
|
||||
} finally {
|
||||
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function setup_events(){
|
||||
|
||||
// When the video is ready, we start streaming it to the canvas.
|
||||
// The canvas is hidden, but it still exists in the browser.
|
||||
video.addEventListener('canplay', function(ev) {
|
||||
if (!streaming) {
|
||||
height = video.videoHeight / (video.videoWidth / width);
|
||||
|
||||
if (isNaN(height)) {
|
||||
height = width / (4 / 3);
|
||||
}
|
||||
|
||||
video.setAttribute('width', width);
|
||||
video.setAttribute('height', height);
|
||||
canvas.setAttribute('width', width);
|
||||
canvas.setAttribute('height', height);
|
||||
|
||||
photo.setAttribute('width', width);
|
||||
photo.setAttribute('height', height);
|
||||
|
||||
|
||||
streaming = true;
|
||||
printButton.removeAttribute("disabled","");
|
||||
|
||||
}
|
||||
}, false);
|
||||
|
||||
switch_cameras.addEventListener('click', function(ev) {
|
||||
alert("Just turn your phone.");
|
||||
}, false );
|
||||
|
||||
printButton.addEventListener('click', function(ev){
|
||||
data = take_picture();
|
||||
print_picture(data)
|
||||
ev.preventDefault();
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
function clear_canvas() {
|
||||
var context = canvas.getContext('2d');
|
||||
context.fillStyle = "#AAA";
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
var data = canvas.toDataURL('image/png');
|
||||
photo.setAttribute('src', data);
|
||||
}
|
||||
|
||||
function dataURLtoFile(dataurl, filename) {
|
||||
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
|
||||
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
||||
while(n--){
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
return new File([u8arr], filename, {type:mime});
|
||||
}
|
||||
|
||||
function take_picture(){
|
||||
var context = canvas.getContext('2d');
|
||||
if (width && height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
var data = canvas.toDataURL('image/png');
|
||||
photo.setAttribute('src', data);
|
||||
} else {
|
||||
clear_canvas();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function print_picture(data){
|
||||
var url = "/api/print/img"
|
||||
var picture = dataURLtoFile(data);
|
||||
const formData = new FormData();
|
||||
let currentDate = new Date();
|
||||
let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
|
||||
|
||||
formData.set("img", picture, "picture.png");
|
||||
formData.set("signature", "Printed via the webcam @ " + time)
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST', // or 'PUT'
|
||||
body: formData,
|
||||
// headers:{
|
||||
// 'Content-Type': 'multipart/form-data'
|
||||
// }
|
||||
}).then(response => console.log('Success:', response), true)
|
||||
.catch(error => console.error('Error:', error), false);
|
||||
|
||||
}
|
||||
|
||||
window.addEventListener('load', startup, false);
|
@ -51,7 +51,9 @@
|
||||
|
||||
{% endblock %}
|
||||
<hr>
|
||||
<footer>Little Prynter is built by <a href="https://n07070.xyz/about-me/">n07070</a> because it's fun :) | <a href="https://git.n07070.xyz/n07070/littleprynter/src/branch/epson-tm-t20iii">Source code - AGPLv3</a></footer>
|
||||
</div>
|
||||
|
||||
<footer class="row">
|
||||
<p class=" text-center">Little Prynter is built by <a href="https://n07070.xyz/about-me/">n07070</a> because it's fun :) | <a href="https://git.n07070.xyz/n07070/littleprynter">Source code - AGPLv3</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -2,29 +2,27 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="card">
|
||||
<h3 class="card-header">Print a short message</h3>
|
||||
<div class="card-body">
|
||||
<div class="card">
|
||||
<h3 class="card-header">Print a short message</h3>
|
||||
<div class="card-body">
|
||||
<form class="form-group" action="/api/print/sms" method="post">
|
||||
<input class="form-control" type="text" name="txt" placeholder="200 chars or less " maxlength="200" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="form-group" action="/api/print/sms" method="post">
|
||||
<input class="form-control" type="text" name="txt" placeholder="200 chars or less " maxlength="200" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
||||
<br>
|
||||
|
||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="card-header">Print an image</h3>
|
||||
<div class="card-body">
|
||||
<form enctype="multipart/form-data" class="form-group" action="/api/print/img" method="post">
|
||||
<input class="form-control" type="file" name="img" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
<div class="card">
|
||||
<h3 class="card-header">Print an image</h3>
|
||||
<div class="card-body">
|
||||
<form enctype="multipart/form-data" class="form-group" action="/api/print/img" method="post">
|
||||
<input class="form-control" type="file" name="img" required><br>
|
||||
<input class="form-control" type="text" name="signature" placeholder="Signature or pseudo" maxlength="200" required><br>
|
||||
<input class="btn btn-primary float-right" type="submit" value="Imprimer" name="imprimer">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -2,13 +2,40 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<noscript class="d-block">
|
||||
|
||||
<div class="alert alert-warning " role="alert">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bug-fill" viewBox="0 0 16 16">
|
||||
<path d="M4.978.855a.5.5 0 1 0-.956.29l.41 1.352A4.985 4.985 0 0 0 3 6h10a4.985 4.985 0 0 0-1.432-3.503l.41-1.352a.5.5 0 1 0-.956-.29l-.291.956A4.978 4.978 0 0 0 8 1a4.979 4.979 0 0 0-2.731.811l-.29-.956z"/>
|
||||
<path d="M13 6v1H8.5v8.975A5 5 0 0 0 13 11h.5a.5.5 0 0 1 .5.5v.5a.5.5 0 1 0 1 0v-.5a1.5 1.5 0 0 0-1.5-1.5H13V9h1.5a.5.5 0 0 0 0-1H13V7h.5A1.5 1.5 0 0 0 15 5.5V5a.5.5 0 0 0-1 0v.5a.5.5 0 0 1-.5.5H13zm-5.5 9.975V7H3V6h-.5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 0-1 0v.5A1.5 1.5 0 0 0 2.5 7H3v1H1.5a.5.5 0 0 0 0 1H3v1h-.5A1.5 1.5 0 0 0 1 11.5v.5a.5.5 0 1 0 1 0v-.5a.5.5 0 0 1 .5-.5H3a5 5 0 0 0 4.5 4.975z"/>
|
||||
</svg>
|
||||
This page will need Javascript and your authorization to use the webcam to work. It is not possible because Javascript seems to be deactived on this page.
|
||||
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div class="card">
|
||||
<h3 class="card-header">Photomaton</h3>
|
||||
<div class="card-body row">
|
||||
|
||||
<canvas id="canvas"></canvas>
|
||||
<video id="video" class="align-self-center">Video stream not available.</video>
|
||||
<div class="image_dither">
|
||||
<div class="row align-self-center">
|
||||
<button class=" btn btn-primary" name="print_picture" id="print_button" disabled="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-printer" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"/>
|
||||
<path d="M5 1a2 2 0 0 0-2 2v2H2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1v1a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-1h1a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-1V3a2 2 0 0 0-2-2H5zM4 3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2H4V3zm1 5a2 2 0 0 0-2 2v1H2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v-1a2 2 0 0 0-2-2H5zm7 2v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class=" btn btn-secondary" name="flip" id="flip" disabled="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
|
||||
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/>
|
||||
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="image_dither" id="image_dither">
|
||||
<picture>
|
||||
<img id="photo" class="align-self-center">
|
||||
</picture>
|
||||
@ -17,21 +44,11 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- <div class="row row-cols-2"> -->
|
||||
<!-- <button class="align-self-center col-3 btn btn-danger" name="take_picture" id="startbutton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-camera" viewBox="0 0 16 16">
|
||||
<path d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1v6zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2z"/>
|
||||
<path d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<br><br> -->
|
||||
<button class="align-self-center col-3 btn btn-danger" name="print_picture" id="print_button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54" fill="currentColor" class="bi bi-printer" viewBox="0 0 16 16">
|
||||
<path d="M2.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"/>
|
||||
<path d="M5 1a2 2 0 0 0-2 2v2H2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1v1a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-1h1a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-1V3a2 2 0 0 0-2-2H5zM4 3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2H4V3zm1 5a2 2 0 0 0-2 2v1H2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v-1a2 2 0 0 0-2-2H5zm7 2v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- </div> -->
|
||||
|
||||
|
||||
<!-- </div> -->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -40,9 +57,9 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
video {
|
||||
/* video {
|
||||
display: none;
|
||||
}
|
||||
} */
|
||||
|
||||
.image_dither {
|
||||
/* width: 300px;
|
||||
@ -80,106 +97,7 @@
|
||||
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
let streaming;
|
||||
var width = document.getElementById("video").parentNode.parentElement.clientWidth;
|
||||
var height = width / (4 / 3);
|
||||
function startup() {
|
||||
video = document.getElementById('video');
|
||||
canvas = document.getElementById('canvas');
|
||||
photo = document.getElementById('photo');
|
||||
printButton = document.getElementById('print_button');
|
||||
|
||||
// access video stream from webcam
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false
|
||||
})
|
||||
// on success, stream it in video tag
|
||||
// the video tag is hidden, as is the canvas.
|
||||
.then(function(stream) {
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log("An error occurred: " + err);
|
||||
});
|
||||
|
||||
video.addEventListener('canplay', function(ev) {
|
||||
if (!streaming) {
|
||||
height = video.videoHeight / (video.videoWidth / width);
|
||||
|
||||
if (isNaN(height)) {
|
||||
height = width / (4 / 3);
|
||||
}
|
||||
|
||||
video.setAttribute('width', width);
|
||||
video.setAttribute('height', height);
|
||||
canvas.setAttribute('width', width);
|
||||
canvas.setAttribute('height', height);
|
||||
|
||||
photo.setAttribute('width', width);
|
||||
photo.setAttribute('height', height);
|
||||
|
||||
|
||||
streaming = true;
|
||||
}
|
||||
}, false);
|
||||
|
||||
printButton.addEventListener('click', function(ev){
|
||||
data = takepicture();
|
||||
printPicture(data)
|
||||
ev.preventDefault();
|
||||
|
||||
}, false)
|
||||
|
||||
}
|
||||
|
||||
function clearphoto() {
|
||||
var context = canvas.getContext('2d');
|
||||
context.fillStyle = "#AAA";
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
var data = canvas.toDataURL('image/png');
|
||||
photo.setAttribute('src', data);
|
||||
}
|
||||
|
||||
function takepicture() {
|
||||
var context = canvas.getContext('2d');
|
||||
if (width && height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
var data = canvas.toDataURL('image/png');
|
||||
photo.setAttribute('src', data);
|
||||
} else {
|
||||
clearphoto();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function printPicture(data){
|
||||
var url = "/api/print/img"
|
||||
var picture = data;
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("file", picture);
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST', // or 'PUT'
|
||||
body: formData,
|
||||
headers:{
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}).then(response => console.log('Success:', response), true)
|
||||
.catch(error => console.error('Error:', error), false);
|
||||
|
||||
}
|
||||
|
||||
window.addEventListener('load', startup, false);
|
||||
</script>
|
||||
<script type="text/javascript" src="{{ url_for('static',filename='js/webcam.js') }}"></script>
|
||||
|
||||
<br>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user