bring back the badge editor

This commit is contained in:
2026-06-05 07:19:53 +03:00
parent c7ba23ad22
commit 6fab93ebeb
10 changed files with 426 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
from flask import Blueprint, render_template, request, url_for
from ..auth import get_active_user, is_logged_in, hard_login_required
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, Threads, Posts
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, Threads, Posts, Badges, BadgeUploads
from functools import wraps
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
@@ -124,3 +124,12 @@ def bookmark_post():
})
return '', 204
@bp.get('/badges/editor/')
@hard_login_required
@user_required
def badge_editor():
user = get_active_user()
badges = Badges.get_for_user(user.id)
badge_uploads = BadgeUploads.get_for_user(user.id)
return render_template('hyper/badge_editor.html', badges=badges, badge_uploads=badge_uploads)

View File

@@ -14,7 +14,7 @@ from ..auth import (
login_required, revoke_session, get_active_user,
parse_display_name, revoke_all_sessions, csrf_verified
)
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections, InviteKeys
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections, InviteKeys, Badges, BadgeUploads
from ..constants import PermissionLevel, InfoboxKind
from ..util import get_form_checkbox, time_now
from ..lib.babycode import babycode_to_html
@@ -24,6 +24,7 @@ import os
import time
AVATAR_MAX_SIZE = 1000 * 1000 # 1MB
BADGE_MAX_SIZE = 1000 * 500 # 500K
bp = Blueprint('users', __name__, url_prefix='/users/')
@@ -60,6 +61,22 @@ def validate_and_create_avatar(input_image, filename):
except WandException:
return False
def validate_and_create_badge(input_image, filename):
try:
with Image(blob=input_image) as img:
if img.width != 88 or img.height != 31:
return False
if hasattr(img, 'sequence') and len(img.sequence) > 1:
img = Image(image=img.sequence[0])
img.strip()
img.format = 'webp'
img.compression_quality = 90
img.save(filename=filename)
return True
except WandException:
return False
def anonymize_user(user_id):
deleted_user = Users.find({'username': 'deleteduser'})
@@ -282,6 +299,7 @@ def posts(username):
'users/posts.html', posts=posts,
page=page, page_count=page_count,
target_user=target_user,
Reactions=Reactions,
)
@bp.get('/<username>/threads/')
@@ -305,6 +323,7 @@ def threads(username):
'users/threads.html', threads=threads,
page=page, page_count=page_count,
target_user=target_user,
Reactions=Reactions,
)
@bp.get('/<username>/comments/')
@@ -611,3 +630,107 @@ def revoke_invite_key(username):
invite.delete()
return redirect(url_for('.settings', username=username, _anchor='invite'))
@bp.post('/<username>/settings/badges/')
@login_required
@redirect_to_own
def save_badges(username):
user = get_active_user()
if user.is_guest():
abort(403)
ids = request.form.getlist('id[]', type=int)
badge_choices = request.form.getlist('badge_choice[]')
files = request.files.getlist('badge_file[]')
labels = request.form.getlist('label[]')
links = request.form.getlist('link[]')
existing_badges = {badge.id: badge for badge in Badges.findall({'user_id': user.id})}
if not (len(ids) == len(badge_choices) == len(files) == len(labels) == len(links)):
abort(400)
rejected_badges = []
# print(ids)
# print(db.query(f'SELECT id FROM badges WHERE id NOT IN {db.binding_list(len(ids))}', *ids))
deleted_badges = Badges.findall([
('id', 'NOT IN', ids),
('user_id', '=', user.id),
])
print(list(map(lambda x: x.id, deleted_badges)))
with db.transaction():
for b in deleted_badges:
b.delete()
for i, id in enumerate(ids):
badge_upload_id = badge_choices[i]
label = labels[i]
link = links[i]
pending_badge = {
'label': label,
'link': link,
'sort_order': i,
}
if badge_upload_id == 'custom':
file = files[i]
if not file:
rejected_badges.append(file.filename)
continue
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0, os.SEEK_SET)
if file_size > BADGE_MAX_SIZE:
rejected_badges.append(file.filename)
continue
file_bytes = file.read()
now = time_now()
filename = f'u{user.id}d{now}s{i}.webp'
output_path = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], filename)
proxied_filename = f'/static/badges/user/{filename}'
res = validate_and_create_badge(file_bytes, output_path)
if not res:
rejected_badges.append(file.filename)
continue
bu = BadgeUploads.create({
'user_id': user.id,
'uploaded_at': now,
'file_path': proxied_filename,
'original_filename': file.filename
})
else:
bu = BadgeUploads.find({'id': badge_upload_id})
if not bu:
continue
pending_badge['upload'] = bu.id
if id == -1:
pending_badge['user_id'] = user.id
badge = Badges.create(pending_badge)
else:
badge = Badges.find({'id': id})
if badge.user_id != user.id:
continue
if not badge:
continue
badge.update(pending_badge)
for stale_upload in BadgeUploads.get_unused_for_user(user.id):
filename = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], os.path.basename(stale_upload.file_path))
os.remove(filename)
stale_upload.delete()
message = 'Badges updated.'
icon = InfoboxKind.INFO
if rejected_badges:
message += f';Some of your badges were incorrect and were not uploaded: {", ".join(rejected_badges)}.'
icon = InfoboxKind.WARN
flash(message, icon)
return redirect(url_for('.settings', username=username))