hat submission UI

remotes/1693045480750635534/spooky-22
Aevann1 2022-09-10 07:37:11 +02:00
parent faac45b425
commit 1a64ba3db8
9 changed files with 377 additions and 78 deletions

View File

@ -13,8 +13,10 @@ class HatDef(Base):
description = Column(String)
author_id = Column(Integer, ForeignKey('users.id'))
price = Column(Integer)
submitter_id = Column(Integer, ForeignKey("users.id"))
author = relationship("User", primaryjoin="HatDef.author_id == User.id", back_populates="designed_hats")
submitter = relationship("User", primaryjoin="HatDef.submitter_id == User.id")
@property
@lazy

View File

@ -212,7 +212,6 @@ SIDEBAR_THREAD = 0
BANNER_THREAD = 0
BADGE_THREAD = 0
SNAPPY_THREAD = 0
HAT_THREAD = 0
if SITE == 'rdrama.net':
FEATURES['PRONOUNS'] = True
@ -222,7 +221,6 @@ if SITE == 'rdrama.net':
BANNER_THREAD = 37697
BADGE_THREAD = 37833
SNAPPY_THREAD = 37749
HAT_THREAD = 100210
HOLE_COST = 50000
HOLE_INACTIVITY_DELETION = True
@ -941,7 +939,7 @@ if path.isfile(f'snappy_{SITE_NAME}.txt'):
YOUTUBE_KEY = environ.get("YOUTUBE_KEY", "").strip()
ADMIGGERS = {SIDEBAR_THREAD, BANNER_THREAD, BADGE_THREAD, SNAPPY_THREAD, HAT_THREAD}
ADMIGGERS = {SIDEBAR_THREAD, BANNER_THREAD, BADGE_THREAD, SNAPPY_THREAD}
proxies = {"http":"http://127.0.0.1:18080","https":"http://127.0.0.1:18080"}

View File

@ -12,11 +12,12 @@ marseyaward_body_regex = re.compile(">[^<\s+]|[^>\s+]<", flags=re.A)
marseyaward_title_regex = re.compile("( *<img[^>]+>)+", flags=re.A)
marsey_regex = re.compile("marsey[a-z0-9]{1,24}", flags=re.A)
hat_regex = re.compile("[a-zA-Z0-9\-() ,_]{1,50}", flags=re.A)
tags_regex = re.compile("[a-z0-9: ]{1,200}", flags=re.A)
hat_regex = re.compile("[a-zA-Z0-9\-() ,_]{1,50}", flags=re.A)
description_regex = re.compile("[^<>&\n\t]{1,300}", flags=re.A)
valid_sub_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$", flags=re.A)

View File

@ -231,51 +231,6 @@ def comment(v):
data=f'{{"files": ["https://{SITE}/assets/images/badges/{badge.id}.webp"]}}', timeout=5)
except Exception as e:
return {"error": str(e)}, 400
elif v.admin_level > 2 and parent_post.id == HAT_THREAD:
try:
hat = loads(body)
name = hat["name"]
if not hat_regex.fullmatch(name): return {"error": "Invalid name!"}, 400
existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none()
if existing: return {"error": "A hat with this name already exists!"}, 403
if "author" in hat: user = get_user(hat["author"])
elif "author_id" in hat: user = get_account(hat["author_id"])
else: abort(400)
filename = f'files/assets/images/hats/{name}.webp'
copyfile(oldname, filename)
process_image(filename, 200)
hat_def = HatDef(
name=name,
description=hat["description"],
author_id=user.id,
price=hat["price"]
)
g.db.add(hat_def)
g.db.flush()
hat = Hat(
user_id=user.id,
hat_id=hat_def.id
)
g.db.add(hat)
all_by_author = g.db.query(HatDef).filter_by(author_id=user.id).count()
if all_by_author >= 250:
badge_grant(badge_id=166, user=user)
elif all_by_author >= 100:
badge_grant(badge_id=165, user=user)
elif all_by_author >= 50:
badge_grant(badge_id=164, user=user)
elif all_by_author >= 10:
badge_grant(badge_id=163, user=user)
except Exception as e:
return {"error": str(e)}, 400
body += f"\n\n![]({image})"
elif file.content_type.startswith('video/'):
body += f"\n\n{process_video(file)}"

View File

@ -441,6 +441,26 @@ def categories_json():
return jsonify(data)
@app.get('/asset_submissions/<image>')
@limiter.exempt
def asset_submissions(image):
if not image.endswith('.webp'): abort(404)
resp = make_response(send_from_directory('/asset_submissions', image))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get("/submit/marseys")
@auth_required
def submit_marseys(v):
@ -526,7 +546,18 @@ def approve_marsey(v, name):
if not tags:
return {"error": "You need to include tags!"}, 400
marsey.name = request.values.get('name').lower().strip()
new_name = request.values.get('name').lower().strip()
if not new_name:
return {"error": "You need to include name!"}, 400
if not marsey_regex.fullmatch(new_name):
return {"error": "Invalid name!"}, 400
if not tags_regex.fullmatch(tags):
return {"error": "Invalid tags!"}, 400
marsey.name = new_name
marsey.tags = tags
g.db.add(marsey)
@ -546,11 +577,13 @@ def approve_marsey(v, name):
data=f'{{"files": ["https://{SITE}/e/{marsey.name}.webp"]}}', timeout=5)
cache.delete_memoized(marsey_list)
msg = f'@{v.username} has approved a marsey you submitted: :{marsey.name}:'
send_repeatable_notification(marsey.submitter_id, msg)
if v.id != marsey.submitter_id:
msg = f"@{v.username} has approved a marsey you submitted: :{marsey.name}:"
send_repeatable_notification(marsey.submitter_id, msg)
marsey.submitter_id = None
return {"message": f"{marsey.name} approved!"}
return {"message": f"'{marsey.name}' approved!"}
@app.post("/admin/reject/marsey/<name>")
@admin_level_required(3)
@ -564,22 +597,158 @@ def reject_marsey(v, name):
if not marsey:
return {"error": f"This marsey '{name}' doesn't exist!"}, 404
msg = f"@{v.username} has rejected a marsey you submitted: `'{marsey.name}'`"
send_repeatable_notification(marsey.submitter_id, msg)
if v.id != marsey.submitter_id:
msg = f"@{v.username} has rejected a marsey you submitted: `'{marsey.name}'`"
send_repeatable_notification(marsey.submitter_id, msg)
g.db.delete(marsey)
os.remove(f"/asset_submissions/{marsey.name}.webp")
return {"message": f"{marsey.name} rejected!"}
return {"message": f"'{marsey.name}' rejected!"}
@app.get('/asset_submissions/<image>')
@limiter.exempt
def asset_submissions(image):
if not image.endswith('.webp'): abort(404)
resp = make_response(send_from_directory('/asset_submissions', image))
resp.headers.remove("Cache-Control")
resp.headers.add("Cache-Control", "public, max-age=3153600")
resp.headers.remove("Content-Type")
resp.headers.add("Content-Type", "image/webp")
return resp
@app.get("/submit/hats")
@auth_required
def submit_hats(v):
if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all()
else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all()
return render_template("submit_hats.html", v=v, hats=hats)
@app.post("/submit/hats")
@auth_required
def submit_hat(v):
def error(error):
if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all()
else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all()
return render_template("submit_hats.html", v=v, hats=hats, error=error), 400
if request.headers.get("cf-ipcountry") == "T1":
return error("Image uploads are not allowed through TOR.")
file = request.files["image"]
if not file or not file.content_type.startswith('image/'):
return error("You need to submit an image!")
name = request.values.get('name').lower().strip()
if not hat_regex.fullmatch(name):
return error("Invalid name!")
existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none()
if existing:
return error("A hat with this name already exists!")
description = request.values.get('description').lower().strip()
if not description_regex.fullmatch(description):
return error("Invalid description!")
author = request.values.get('author').strip()
author = get_user(author)
highquality = f'/asset_submissions/{name}.png'
file.save(highquality)
i = Image.open(highquality)
if i.width > 100 or i.height > 130:
return error("Images must be 100x130")
filename = f'/asset_submissions/{name}.webp'
copyfile(highquality, filename)
process_image(filename)
hat = HatDef(name=name, author_id=author.id, description=description, price=500, submitter_id=v.id)
g.db.add(hat)
g.db.commit()
if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all()
else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all()
return render_template("submit_hats.html", v=v, hats=hats, msg=f"'{name}' submitted successfully!")
@app.post("/admin/approve/hat/<name>")
@admin_level_required(3)
def approve_hat(v, name):
if CARP_ID and v.id != CARP_ID:
return {"error": "Only Carp can approve hats!"}, 403
name = name.lower().strip()
hat = g.db.query(HatDef).filter_by(name=name).one_or_none()
if not hat:
return {"error": f"This hat '{name}' doesn't exist!"}, 404
description = request.values.get('description').lower().strip()
if not description:
return {"error": "You need to include description!"}, 400
new_name = request.values.get('name').lower().strip()
if not new_name:
return {"error": "You need to include name!"}, 400
if not hat_regex.fullmatch(new_name):
return {"error": "Invalid name!"}, 400
if not description_regex.fullmatch(description):
return {"error": "Invalid description!"}, 400
hat.name = new_name
hat.description = description
g.db.add(hat)
move(f"/asset_submissions/{name}.webp", f"files/assets/images/hats/{hat.name}.webp")
g.db.flush()
author = hat.author
all_by_author = g.db.query(HatDef).filter_by(author_id=author.id).count()
if all_by_author >= 250:
badge_grant(badge_id=166, user=author)
elif all_by_author >= 100:
badge_grant(badge_id=165, user=author)
elif all_by_author >= 50:
badge_grant(badge_id=164, user=author)
elif all_by_author >= 10:
badge_grant(badge_id=163, user=author)
hat_copy = Hat(
user_id=author.id,
hat_id=hat.id
)
g.db.add(hat_copy)
if v.id != hat.submitter_id:
msg = f"@{v.username} has approved a hat you submitted: '{hat.name}'"
send_repeatable_notification(hat.submitter_id, msg)
hat.submitter_id = None
return {"message": f"'{hat.name}' approved!"}
@app.post("/admin/reject/hat/<name>")
@admin_level_required(3)
def reject_hat(v, name):
if CARP_ID and v.id != CARP_ID:
return {"error": "Only Carp can reject hats!"}, 403
name = name.lower().strip()
hat = g.db.query(HatDef).filter_by(name=name).one_or_none()
if not hat:
return {"error": f"This hat '{name}' doesn't exist!"}, 404
if v.id != hat.submitter_id:
msg = f"@{v.username} has rejected a hat you submitted: `'{hat.name}'`"
send_repeatable_notification(hat.submitter_id, msg)
g.db.delete(hat)
os.remove(f"/asset_submissions/{hat.name}.webp")
return {"message": f"'{hat.name}' rejected!"}

View File

@ -52,7 +52,7 @@
'Submit Hats',
'Submit a Hat to be added.',
'fa-hat-cowboy', '#7c603e',
'/post/100209',
'/submit/hats',
)
])-%}
{%- endif -%}

View File

@ -0,0 +1,168 @@
{% extends "default.html" %}
{% block title %}
<title>Submit Hats</title>
{% endblock %}
{% block pagetype %}message{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-danger alert-dismissible fade show mb-3 mt-4" role="alert">
<i class="fas fa-exclamation-circle my-auto"></i>
<span>
{{error}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
{% if msg %}
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
<span>
{{msg}}
</span>
<button class="close" data-bs-dismiss="alert" aria-label="Close">
<span aria-hidden="true"><i class="far fa-times"></i></span>
</button>
</div>
{% endif %}
<div class="mx-4">
<h2 class="mt-5">Submit Hat</h2>
<div class="settings-section rounded">
<div class="d-lg-flex">
<div class="body w-lg-100">
<form action="/submit/hats" method="post" enctype="multipart/form-data">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<div id="image-upload-block">
<div><label class="mt-3">Image</label></div>
<img loading="lazy" id="image-preview" style="max-width:50%">
<label class="btn btn-secondary m-0" for="file-upload">
<div id="filename-show">Select Image</div>
<input autocomplete="off" id="file-upload" accept="image/*" type="file" name="image" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} hidden>
</label>
</div>
<label class="mt-3" for="name">Name</label>
<input autocomplete="off" type="text" id="name" class="form-control" name="name" maxlength="50" pattern='[a-zA-Z0-9\-() ,_]{1,50}' required>
<label class="mt-3" for="author">Author</label>
<input autocomplete="off" type="text" id="author" class="form-control" name="author" value="{{v.username.lower()}}" maxlength="30" pattern='[a-z0-9_\-]{3,30}' required>
<label class="mt-3" for="description">Description</label>
<input autocomplete="off" type="text" id="description" class="form-control" name="description" maxlength="300" pattern='[^<>]{1,300}' required>
<div class="footer mt-5">
<div class="d-flex">
<input id="submit-hat" disabled type="submit" onclick="disable(this)" class="btn btn-primary ml-auto" value="Submit Hat">
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.onpaste = function(event) {
files = event.clipboardData.files
filename = files[0]
if (filename)
{
filename = filename.name.toLowerCase()
f=document.getElementById('file-upload');
f.files = files;
document.getElementById('filename-show').textContent = filename;
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".gif") || filename.endsWith(".webp"))
{
var fileReader = new FileReader();
fileReader.readAsDataURL(f.files[0]);
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
}
document.getElementById('file-upload').setAttribute('required', 'false');
}
}
document.getElementById('file-upload').addEventListener('change', function(){
f=document.getElementById('file-upload');
document.getElementById('filename-show').textContent = document.getElementById('file-upload').files[0].name.substr(0, 20);
filename = f.files[0].name.toLowerCase()
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".gif") || filename.endsWith(".webp"))
{
var fileReader = new FileReader();
fileReader.readAsDataURL(f.files[0]);
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
document.getElementById('submit-hat').disabled = false;
}
})
function approve_hat(t, name) {
t.disabled = true;
t.classList.add("disabled");
post_toast_callback(`/admin/approve/hat/${name}`,
{
"description": document.getElementById(`${name}-description`).value,
"name": document.getElementById(`${name}-name`).value,
"price": document.getElementById(`${name}-price`).value,
},
(xhr) => {
if(xhr.status == 200) {
document.getElementById(`${name}-hat`).classList.add('d-none')
}
}
);
setTimeout(() => {
t.disabled = false;
t.classList.remove("disabled");
}, 2000);
}
</script>
<h2 class="mt-5 mx-4">Pending Carp Approval</h2>
<div class="row mt-5 mx-4">
<div class="col px-0">
<div class="settings">
{% for hat in hats %}
<div id="{{hat.name}}-hat" class="settings-section rounded">
<div class="d-lg-flex">
<div class="body w-lg-100">
<input type="hidden" name="formkey" value="{{v.formkey}}">
<div><label class="mt-3">Image</label></div>
<img loading="lazy" src="/asset_submissions/{{hat.name}}.webp" style="max-width:50%">
<div><label class="mt-3" for="{{hat.name}}-submitter">Submitter</label></div>
<input autocomplete="off" type="text" id="{{hat.name}}-submitter" class="form-control" maxlength="30" value="{{hat.submitter.username}}" readonly>
<label class="mt-3" for="{{hat.name}}-author">Author</label>
<input autocomplete="off" type="text" id="{{hat.name}}-author" class="form-control" maxlength="30" value="{{hat.author.username}}" readonly>
<label class="mt-3" for="{{hat.name}}-name">Name</label>
<input autocomplete="off" type="text" id="{{hat.name}}-name" class="form-control" name="name" maxlength="30" value="{{hat.name}}" pattern='hat[a-z0-9]{1,24}' required>
<label class="mt-3" for="{{hat.name}}-description">Description</label>
<input autocomplete="off" type="text" id="{{hat.name}}-description" class="form-control" name="description" maxlength="300" value="{{hat.description}}" pattern='[^<>&\n\t]{1,300}' required>
<div><label class="mt-3" for="{{hat.name}}-price">Price</label></div>
<input autocomplete="off" type="number" id="{{hat.name}}-price" class="form-control" name="price" min="0" value="{{hat.price}}" required>
</div>
</div>
{% if v.admin_level > 2 %}
<div class="d-flex my-4 mx-3">
<a role="button" class="btn btn-secondary ml-auto mr-2" onclick="post_toast(this,'/admin/reject/hat/{{hat.name}}', true)">Reject</a>
<a role="button" class="btn btn-primary mr-0" onclick="approve_hat(this, '{{hat.name}}')">Approve</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -143,23 +143,23 @@
<div><label class="mt-3">Image</label></div>
<img loading="lazy" src="/asset_submissions/{{marsey.name}}.webp" style="max-width:50%">
<div><label class="mt-3" for="{{marsey.name}}-name">Name</label></div>
<input autocomplete="off" type="text" id="{{marsey.name}}-name" class="form-control" name="name" maxlength="30" value="{{marsey.name}}" pattern='marsey[a-z0-9]{1,24}' required>
<div><label class="mt-3" for="{{marsey.name}}-submitter">Submitter</label></div>
<input autocomplete="off" type="text" id="{{marsey.name}}-submitter" class="form-control" maxlength="30" value="{{marsey.submitter}}" readonly>
<label class="mt-3" for="{{marsey.name}}-author">Author</label>
<input autocomplete="off" type="text" id="{{marsey.name}}-author" class="form-control" maxlength="30" value="{{marsey.author}}" readonly>
<label class="mt-3" for="{{marsey.name}}-name">Name</label>
<input autocomplete="off" type="text" id="{{marsey.name}}-name" class="form-control" name="name" maxlength="30" value="{{marsey.name}}" pattern='marsey[a-z0-9]{1,24}' required>
<label class="mt-3" for="{{marsey.name}}-tags">Tags</label>
<input autocomplete="off" type="text" id="{{marsey.name}}-tags" class="form-control" name="tags" maxlength="200" value="{{marsey.tags}}" pattern='[a-z0-9: ]{1,200}' required>
<label class="mt-3" for="{{marsey.name}}-submitter">Submitter</label>
<input autocomplete="off" type="text" id="{{marsey.name}}-submitter" class="form-control" maxlength="30" value="{{marsey.submitter}}" readonly>
</div>
</div>
{% if v.admin_level > 2 %}
<div class="d-flex my-4 mx-3">
<a role="button" class="btn btn-secondary mr-0" onclick="post_toast(this,'/admin/reject/marsey/{{marsey.name}}', true)">Reject</a>
<a role="button" class="btn btn-primary ml-auto mr-2" onclick="approve_marsey(this, '{{marsey.name}}')">Approve</a>
<a role="button" class="btn btn-secondary ml-auto mr-2" onclick="post_toast(this,'/admin/reject/marsey/{{marsey.name}}', true)">Reject</a>
<a role="button" class="btn btn-primary mr-0" onclick="approve_marsey(this, '{{marsey.name}}')">Approve</a>
</div>
{% endif %}
</div>

View File

@ -0,0 +1,6 @@
alter table hat_defs add column submitter_id int;
ALTER TABLE ONLY public.hat_defs
ADD CONSTRAINT hat_def_submitter_fkey FOREIGN KEY (submitter_id) REFERENCES public.users(id);
CREATE INDEX hat_defs_submitter_id_idx ON public.hat_defs USING btree (submitter_id);