From 8c622c04c716911bec9ee4ab1a9a08fc760f9ba4 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 1 Jan 2024 14:49:15 +1300 Subject: [PATCH] admin users --- app/activitypub/routes.py | 66 +++--- app/activitypub/util.py | 1 + app/admin/forms.py | 20 +- app/admin/routes.py | 204 +++++++++++++++--- app/auth/routes.py | 2 +- app/models.py | 19 +- app/static/styles.css | 4 - app/static/styles.scss | 2 +- app/templates/admin/_nav.html | 1 + app/templates/admin/edit_user.html | 46 ++++ app/templates/admin/users.html | 60 ++++++ app/templates/base.html | 2 +- app/templates/email/reset_password.html | 2 +- app/templates/email/reset_password.txt | 2 +- app/templates/email/verification.html | 2 +- app/templates/email/verification.txt | 2 +- app/templates/post/add_reply.html | 2 +- app/templates/post/continue_discussion.html | 2 +- app/templates/post/post.html | 4 +- app/templates/post/post_options.html | 4 +- app/templates/post/post_reply_edit.html | 2 +- app/templates/user/delete_account.html | 2 +- app/templates/user/edit_profile.html | 4 +- app/templates/user/edit_settings.html | 2 +- app/templates/user/show_profile.html | 8 +- app/user/forms.py | 1 + app/user/routes.py | 17 ++ .../versions/0dadae40281d_user_title.py | 32 +++ 28 files changed, 422 insertions(+), 93 deletions(-) create mode 100644 app/templates/admin/edit_user.html create mode 100644 app/templates/admin/users.html create mode 100644 migrations/versions/0dadae40281d_user_title.py diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index ffa9cf43..7d12faea 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -18,7 +18,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \ update_post_from_activity, undo_vote, undo_downvote from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \ - domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text + domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address import werkzeug.exceptions @@ -162,6 +162,7 @@ def user_profile(actor): "type": "Person", "id": f"https://{server}/u/{actor}", "preferredUsername": actor, + "name": user.title if user.title else user.user_name, "inbox": f"https://{server}/u/{actor}/inbox", "outbox": f"https://{server}/u/{actor}/outbox", "discoverable": user.searchable, @@ -309,9 +310,9 @@ def shared_inbox(): # When a user is deleted, the only way to be fairly sure they get deleted everywhere is to tell the whole fediverse. if 'type' in request_json and request_json['type'] == 'Delete' and request_json['id'].endswith('#delete'): if current_app.debug: - process_delete_request(request_json, activity_log.id) + process_delete_request(request_json, activity_log.id, ip_address()) else: - process_delete_request.delay(request_json, activity_log.id) + process_delete_request.delay(request_json, activity_log.id, ip_address()) return '' else: activity_log.activity_id = '' @@ -323,9 +324,9 @@ def shared_inbox(): if actor is not None: if HttpSignature.verify_request(request, actor.public_key, skip_date=True): if current_app.debug: - process_inbox_request(request_json, activity_log.id) + process_inbox_request(request_json, activity_log.id, ip_address()) else: - process_inbox_request.delay(request_json, activity_log.id) + process_inbox_request.delay(request_json, activity_log.id, ip_address()) return '' else: activity_log.exception_message = 'Could not verify signature' @@ -340,7 +341,7 @@ def shared_inbox(): @celery.task -def process_inbox_request(request_json, activitypublog_id): +def process_inbox_request(request_json, activitypublog_id, ip_address): with current_app.app_context(): activity_log = ActivityPubLog.query.get(activitypublog_id) site = Site.query.get(1) # can't use g.site because celery doesn't use Flask's g variable @@ -667,6 +668,7 @@ def process_inbox_request(request_json, activitypublog_id): user.flush_cache() if user.instance_id: user.instance.last_seen = utcnow() + user.instance.ip_address = ip_address # if 'community' in vars() and community is not None: # community.flush_cache() if 'post' in vars() and post is not None: @@ -680,7 +682,7 @@ def process_inbox_request(request_json, activitypublog_id): @celery.task -def process_delete_request(request_json, activitypublog_id): +def process_delete_request(request_json, activitypublog_id, ip_address): with current_app.app_context(): activity_log = ActivityPubLog.query.get(activitypublog_id) if 'type' in request_json and request_json['type'] == 'Delete': @@ -688,37 +690,25 @@ def process_delete_request(request_json, activitypublog_id): user = User.query.filter_by(ap_profile_id=actor_to_delete).first() if user: # check that the user really has been deleted, to avoid spoofing attacks - if not user.is_local() and user_removed_from_remote_server(actor_to_delete, user.instance.software == 'PieFed'): - # Delete all their images to save moderators from having to see disgusting stuff. - files = File.query.join(Post).filter(Post.user_id == user.id).all() - for file in files: - file.delete_from_disk() - file.source_url = '' - if user.avatar_id: - user.avatar.delete_from_disk() - user.avatar.source_url = '' - if user.cover_id: - user.cover.delete_from_disk() - user.cover.source_url = '' - user.banned = True - user.deleted = True - activity_log.result = 'success' - - instances = Instance.query.all() - site = Site.query.get(1) - payload = { - "@context": default_context(), - "actor": user.ap_profile_id, - "id": f"{user.ap_profile_id}#delete", - "object": user.ap_profile_id, - "to": [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "type": "Delete" - } - for instance in instances: - if instance.inbox: - post_request(instance.inbox, payload, site.private_key, f"https://{current_app.config['SERVER_NAME']}#main-key") + if not user.is_local(): + if user_removed_from_remote_server(actor_to_delete, is_piefed=user.instance.software == 'PieFed'): + # Delete all their images to save moderators from having to see disgusting stuff. + files = File.query.join(Post).filter(Post.user_id == user.id).all() + for file in files: + file.delete_from_disk() + file.source_url = '' + if user.avatar_id: + user.avatar.delete_from_disk() + user.avatar.source_url = '' + if user.cover_id: + user.cover.delete_from_disk() + user.cover.source_url = '' + user.banned = True + user.deleted = True + activity_log.result = 'success' + else: + activity_log.result = 'ignored' + activity_log.exception_message = 'User not actually deleted.' else: activity_log.result = 'ignored' activity_log.exception_message = 'Only remote users can be deleted remotely' diff --git a/app/activitypub/util.py b/app/activitypub/util.py index fa0fdac7..9e8879f5 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -322,6 +322,7 @@ def refresh_user_profile_task(user_id): def actor_json_to_model(activity_json, address, server): if activity_json['type'] == 'Person': user = User(user_name=activity_json['preferredUsername'], + title=activity_json['name'] if 'name' in activity_json else None, email=f"{address}@{server}", about_html=parse_summary(activity_json), matrix_user_id=activity_json['matrixUserId'] if 'matrixUserId' in activity_json else '', diff --git a/app/admin/forms.py b/app/admin/forms.py index 3b7a5308..c81d3bca 100644 --- a/app/admin/forms.py +++ b/app/admin/forms.py @@ -2,7 +2,7 @@ from flask_wtf import FlaskForm from flask_wtf.file import FileRequired, FileAllowed from wtforms import StringField, PasswordField, SubmitField, HiddenField, BooleanField, TextAreaField, SelectField, \ FileField, IntegerField -from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length +from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional from flask_babel import _, lazy_gettext as _l @@ -76,4 +76,20 @@ class EditCommunityForm(FlaskForm): if '-' in self.url.data.strip(): self.url.errors.append(_('- cannot be in Url. Use _ instead?')) return False - return True \ No newline at end of file + return True + + +class EditUserForm(FlaskForm): + about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)]) + matrix_user_id = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)]) + profile_file = FileField(_('Avatar image')) + banner_file = FileField(_('Top banner image')) + bot = BooleanField(_l('This profile is a bot')) + newsletter = BooleanField(_l('Subscribe to email newsletter')) + ignore_bots = BooleanField(_l('Hide posts by bots')) + nsfw = BooleanField(_l('Show NSFW posts')) + nsfl = BooleanField(_l('Show NSFL posts')) + searchable = BooleanField(_l('Show profile in user list')) + indexable = BooleanField(_l('Allow search engines to index this profile')) + manually_approves_followers = BooleanField(_l('Manually approve followers')) + submit = SubmitField(_l('Save')) diff --git a/app/admin/routes.py b/app/admin/routes.py index dcccb7d6..91e4c59c 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -9,10 +9,12 @@ from sqlalchemy import text, desc from app import db, celery from app.activitypub.routes import process_inbox_request, process_delete_request from app.activitypub.signature import post_request -from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm +from app.activitypub.util import default_context +from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm from app.community.util import save_icon_file, save_banner_file -from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, User -from app.utils import render_template, permission_required, set_setting, get_setting, gibberish +from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \ + User, Instance, File +from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html from app.admin import bp @@ -157,9 +159,9 @@ def activity_replay(activity_id): activity = ActivityPubLog.query.get_or_404(activity_id) request_json = json.loads(activity.activity_json) if 'type' in request_json and request_json['type'] == 'Delete' and request_json['id'].endswith('#delete'): - process_delete_request(request_json, activity.id) + process_delete_request(request_json, activity.id, None) else: - process_inbox_request(request_json, activity.id) + process_inbox_request(request_json, activity.id, None) return 'Ok' @@ -259,27 +261,177 @@ def unsubscribe_everyone_then_delete_task(community_id): members = CommunityMember.query.filter_by(community_id=community_id).all() for member in members: user = User.query.get(member.user_id) - undo_id = f"https://{current_app.config['SERVER_NAME']}/activities/undo/" + gibberish(15) - follow = { - "actor": f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}", - "to": [community.ap_profile_id], - "object": community.ap_profile_id, - "type": "Follow", - "id": f"https://{current_app.config['SERVER_NAME']}/activities/follow/{gibberish(15)}" - } - undo = { - 'actor': user.profile_id(), - 'to': [community.ap_profile_id], - 'type': 'Undo', - 'id': undo_id, - 'object': follow - } - activity = ActivityPubLog(direction='out', activity_id=undo_id, activity_type='Undo', activity_json=json.dumps(undo), result='processing') - db.session.add(activity) - db.session.commit() - post_request(community.ap_inbox_url, undo, user.private_key, user.profile_id() + '#main-key') - activity.result = 'success' - db.session.commit() + unsubscribe_from_community(community, user) + else: + # todo: federate delete of local community out to all following instances + ... + sleep(5) community.delete_dependencies() db.session.delete(community) # todo: when a remote community is deleted it will be able to be re-created by using the 'Add remote' function. Not ideal. Consider soft-delete. + + +@bp.route('/users', methods=['GET']) +@login_required +@permission_required('administer all users') +def admin_users(): + + page = request.args.get('page', 1, type=int) + + users = User.query.filter_by(deleted=False).order_by(User.user_name).paginate(page=page, per_page=1000, error_out=False) + + next_url = url_for('admin.admin_users', page=users.next_num) if users.has_next else None + prev_url = url_for('admin.admin_users', page=users.prev_num) if users.has_prev and page != 1 else None + + return render_template('admin/users.html', title=_('Users'), next_url=next_url, prev_url=prev_url, users=users) + + +@bp.route('/user//edit', methods=['GET', 'POST']) +@login_required +@permission_required('administer all users') +def admin_user_edit(user_id): + form = EditUserForm() + user = User.query.get_or_404(user_id) + if form.validate_on_submit(): + user.about = form.about.data + user.about_html = markdown_to_html(form.about.data) + user.matrix_user_id = form.matrix_user_id.data + user.bot = form.bot.data + profile_file = request.files['profile_file'] + if profile_file and profile_file.filename != '': + # remove old avatar + file = File.query.get(user.avatar_id) + file.delete_from_disk() + user.avatar_id = None + db.session.delete(file) + + # add new avatar + file = save_icon_file(profile_file, 'users') + if file: + user.avatar = file + banner_file = request.files['banner_file'] + if banner_file and banner_file.filename != '': + # remove old cover + file = File.query.get(user.cover_id) + file.delete_from_disk() + user.cover_id = None + db.session.delete(file) + + # add new cover + file = save_banner_file(banner_file, 'users') + if file: + user.cover = file + user.newsletter = form.newsletter.data + user.ignore_bots = form.ignore_bots.data + user.show_nsfw = form.nsfw.data + user.show_nsfl = form.nsfl.data + user.searchable = form.searchable.data + user.indexable = form.indexable.data + user.ap_manually_approves_followers = form.manually_approves_followers.data + db.session.commit() + user.flush_cache() + flash(_('Saved')) + return redirect(url_for('admin.admin_users')) + else: + if not user.is_local(): + flash(_('This is a remote user - most settings here will be regularly overwritten with data from the original server.'), 'warning') + form.about.data = user.about + form.matrix_user_id.data = user.matrix_user_id + form.newsletter.data = user.newsletter + form.bot.data = user.bot + form.ignore_bots.data = user.ignore_bots + form.nsfw.data = user.show_nsfw + form.nsfl.data = user.show_nsfl + form.searchable.data = user.searchable + form.indexable.data = user.indexable + form.manually_approves_followers.data = user.ap_manually_approves_followers + + return render_template('admin/edit_user.html', title=_('Edit user'), form=form, user=user) + + +@bp.route('/user//delete', methods=['GET']) +@login_required +@permission_required('administer all users') +def admin_user_delete(user_id): + user = User.query.get_or_404(user_id) + + user.banned = True # Unsubscribing everyone could take a long time so until that is completed hide this user from the UI by banning it. + user.last_active = utcnow() + db.session.commit() + + if user.is_local(): + unsubscribe_from_everything_then_delete(user.id) + else: + user.deleted = True + user.delete_dependencies() + db.session.commit() + + flash(_('User deleted')) + return redirect(url_for('admin.admin_users')) + + +def unsubscribe_from_everything_then_delete(user_id): + if current_app.debug: + unsubscribe_from_everything_then_delete_task(user_id) + else: + unsubscribe_from_everything_then_delete_task.delay(user_id) + + +@celery.task +def unsubscribe_from_everything_then_delete_task(user_id): + user = User.query.get(user_id) + if user: + + # unsubscribe + communities = CommunityMember.query.filter_by(user_id=user_id).all() + for membership in communities: + community = Community.query.get(membership.community_id) + unsubscribe_from_community(community, user) + + # federate deletion of account + if user.is_local(): + instances = Instance.query.all() + site = Site.query.get(1) + payload = { + "@context": default_context(), + "actor": user.ap_profile_id, + "id": f"{user.ap_profile_id}#delete", + "object": user.ap_profile_id, + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Delete" + } + for instance in instances: + if instance.inbox and instance.id != 1: + post_request(instance.inbox, payload, site.private_key, + f"https://{current_app.config['SERVER_NAME']}#main-key") + + user.deleted = True + user.delete_dependencies() + db.session.commit() + + +def unsubscribe_from_community(community, user): + undo_id = f"https://{current_app.config['SERVER_NAME']}/activities/undo/" + gibberish(15) + follow = { + "actor": f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}", + "to": [community.ap_profile_id], + "object": community.ap_profile_id, + "type": "Follow", + "id": f"https://{current_app.config['SERVER_NAME']}/activities/follow/{gibberish(15)}" + } + undo = { + 'actor': user.profile_id(), + 'to': [community.ap_profile_id], + 'type': 'Undo', + 'id': undo_id, + 'object': follow + } + activity = ActivityPubLog(direction='out', activity_id=undo_id, activity_type='Undo', + activity_json=json.dumps(undo), result='processing') + db.session.add(activity) + db.session.commit() + post_request(community.ap_inbox_url, undo, user.private_key, user.profile_id() + '#main-key') + activity.result = 'success' + db.session.commit() diff --git a/app/auth/routes.py b/app/auth/routes.py index 6613092b..986b4401 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -91,7 +91,7 @@ def register(): else: verification_token = random_token(16) form.user_name.data = form.user_name.data.strip() - user = User(user_name=form.user_name.data, email=form.real_email.data, + user = User(user_name=form.user_name.data, title=form.user_name.data, email=form.real_email.data, verification_token=verification_token, instance=1, ipaddress=ip_address(), banned=user_ip_banned() or user_cookie_banned()) user.set_password(form.password.data) diff --git a/app/models.py b/app/models.py index a74971df..7d90399c 100644 --- a/app/models.py +++ b/app/models.py @@ -235,6 +235,7 @@ class User(UserMixin, db.Model): query_class = FullTextSearchQuery id = db.Column(db.Integer, primary_key=True) user_name = db.Column(db.String(255), index=True) + title = db.Column(db.String(256)) email = db.Column(db.String(255), index=True) password_hash = db.Column(db.String(128)) verified = db.Column(db.Boolean, default=False) @@ -305,7 +306,10 @@ class User(UserMixin, db.Model): def display_name(self): if self.deleted is False: - return self.user_name + if self.title: + return self.title + else: + return self.user_name else: return '[deleted]' @@ -469,10 +473,23 @@ class User(UserMixin, db.Model): cache.delete('/u/' + self.user_name + '_False') cache.delete('/u/' + self.user_name + '_True') + def delete_dependencies(self): + if self.cover_id: + file = File.query.get(self.cover_id) + file.delete_from_disk() + self.cover_id = None + db.session.delete(file) + if self.avatar_id: + file = File.query.get(self.avatar_id) + file.delete_from_disk() + self.avatar_id = None + db.session.delete(file) + def purge_content(self): files = File.query.join(Post).filter(Post.user_id == self.id).all() for file in files: file.delete_from_disk() + self.delete_dependencies() db.session.query(Report).filter(Report.reporter_id == self.id).delete() db.session.query(Report).filter(Report.suspect_user_id == self.id).delete() db.session.query(ActivityLog).filter(ActivityLog.user_id == self.id).delete() diff --git a/app/static/styles.css b/app/static/styles.css index a74ac8e6..e01fe1bc 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -513,10 +513,6 @@ nav.navbar { color: #777; } -.comment_author a { - font-weight: bold; -} - .low_score .hide_button a, .low_score .comment_author a { font-weight: normal; color: #777; diff --git a/app/static/styles.scss b/app/static/styles.scss index 4607a293..24ff54ac 100644 --- a/app/static/styles.scss +++ b/app/static/styles.scss @@ -186,7 +186,7 @@ nav.navbar { .comment_author { a { - font-weight: bold; + } } diff --git a/app/templates/admin/_nav.html b/app/templates/admin/_nav.html index 966e5a86..4757da5d 100644 --- a/app/templates/admin/_nav.html +++ b/app/templates/admin/_nav.html @@ -3,6 +3,7 @@ {{ _('Site profile') }} | {{ _('Misc settings') }} | {{ _('Communities') }} | + {{ _('Users') }} | {{ _('Federation') }} | {{ _('Activities') }} diff --git a/app/templates/admin/edit_user.html b/app/templates/admin/edit_user.html new file mode 100644 index 00000000..17fd7cff --- /dev/null +++ b/app/templates/admin/edit_user.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% from 'bootstrap/form.html' import render_field %} + +{% block app_content %} +
+
+ {% include 'admin/_nav.html' %} +
+
+ +
+ +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/users.html b/app/templates/admin/users.html new file mode 100644 index 00000000..13b89361 --- /dev/null +++ b/app/templates/admin/users.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+
+ {% include 'admin/_nav.html' %} +
+
+ +
+
+
+ + + + +
+ + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} +
NameLocal/RemoteAttitudeBannedActions
+ {{ user.display_name() }}{{ 'Local' if user.is_local() else 'Remote' }}{{ user.attitude * 100 }}{{ 'Banned'|safe if user.banned }} View local | + {% if not user.is_local() %} + View remote | + {% else %} + View remote | + {% endif %} + Edit | + Delete +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 143b0171..415da616 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -7,7 +7,7 @@ Avatar {% endif %} - {{ user.user_name }} + {{ user.display_name() }} {% if user.created_recently() %} {% endif %} diff --git a/app/templates/email/reset_password.html b/app/templates/email/reset_password.html index d791db59..5370d826 100644 --- a/app/templates/email/reset_password.html +++ b/app/templates/email/reset_password.html @@ -1,4 +1,4 @@ -

Hi {{ user.user_name }},

+

Hi {{ user.display_name() }},

To reset your password diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt index 4eb4a2a9..c6fc8ebf 100644 --- a/app/templates/email/reset_password.txt +++ b/app/templates/email/reset_password.txt @@ -1,4 +1,4 @@ -Hello {{ user.user_name }}, +Hello {{ user.display_name() }}, To reset your password click on the following link: diff --git a/app/templates/email/verification.html b/app/templates/email/verification.html index 8fb8cb40..5eb7a158 100644 --- a/app/templates/email/verification.html +++ b/app/templates/email/verification.html @@ -1,4 +1,4 @@ -

Hi {{ user.user_name }},

+

Hi {{ user.display_name() }},

To verify your email address diff --git a/app/templates/email/verification.txt b/app/templates/email/verification.txt index cb9cb6da..5a20744d 100644 --- a/app/templates/email/verification.txt +++ b/app/templates/email/verification.txt @@ -1,4 +1,4 @@ -Hello {{ user.user_name }}, +Hello {{ user.display_name() }}, To verify your email address: diff --git a/app/templates/post/add_reply.html b/app/templates/post/add_reply.html index 93822562..73c99a6d 100644 --- a/app/templates/post/add_reply.html +++ b/app/templates/post/add_reply.html @@ -59,7 +59,7 @@

Moderators

    {% for mod in mods %} -
  1. {{ mod.user_name }}
  2. +
  3. {{ mod.display_name() }}
  4. {% endfor %}
{% endif %} diff --git a/app/templates/post/continue_discussion.html b/app/templates/post/continue_discussion.html index 6ec78eb6..282fa566 100644 --- a/app/templates/post/continue_discussion.html +++ b/app/templates/post/continue_discussion.html @@ -89,7 +89,7 @@

Moderators

    {% for mod in mods %} -
  1. {{ mod.user_name }}
  2. +
  3. {{ mod.display_name() }}
  4. {% endfor %}
{% endif %} diff --git a/app/templates/post/post.html b/app/templates/post/post.html index 91430eae..45d2b0c7 100644 --- a/app/templates/post/post.html +++ b/app/templates/post/post.html @@ -71,7 +71,7 @@ Avatar {% endif %} - {{ comment['comment'].author.user_name }} + {{ comment['comment'].author.display_name() }} {% endif %} {% if comment['comment'].author.created_recently() %} @@ -160,7 +160,7 @@

Moderators

{% endif %} diff --git a/app/templates/post/post_options.html b/app/templates/post/post_options.html index 68bf4440..8a2ed83d 100644 --- a/app/templates/post/post_options.html +++ b/app/templates/post/post_options.html @@ -31,9 +31,9 @@ {{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }} {% endif %} {% endif %} -
  • - {{ _('Report to moderators') }}
  • {% endif %} +
  • + {{ _('Report to moderators') }}
  • {{ _('If you want to perform more than one of these (e.g. block and report), hold down Ctrl and click, then complete the operation in the new tabs that open.') }}

    diff --git a/app/templates/post/post_reply_edit.html b/app/templates/post/post_reply_edit.html index b85ecb93..4a92fe9f 100644 --- a/app/templates/post/post_reply_edit.html +++ b/app/templates/post/post_reply_edit.html @@ -62,7 +62,7 @@

    Moderators

      {% for mod in mods %} -
    1. {{ mod.user_name }}
    2. +
    3. {{ mod.display_name() }}
    4. {% endfor %}
    {% endif %} diff --git a/app/templates/user/delete_account.html b/app/templates/user/delete_account.html index b83e9d78..16cbfe2f 100644 --- a/app/templates/user/delete_account.html +++ b/app/templates/user/delete_account.html @@ -7,7 +7,7 @@ diff --git a/app/templates/user/edit_profile.html b/app/templates/user/edit_profile.html index 6a08a23f..6c7b71f8 100644 --- a/app/templates/user/edit_profile.html +++ b/app/templates/user/edit_profile.html @@ -7,14 +7,14 @@

    {{ _('Edit profile of %(name)s', name=user.user_name) }}

    {{ form.csrf_token() }} - + {{ render_field(form.title) }} {{ render_field(form.email) }} {{ render_field(form.password_field) }}
    diff --git a/app/templates/user/edit_settings.html b/app/templates/user/edit_settings.html index 81c71c71..9c0f04f2 100644 --- a/app/templates/user/edit_settings.html +++ b/app/templates/user/edit_settings.html @@ -7,7 +7,7 @@ diff --git a/app/templates/user/show_profile.html b/app/templates/user/show_profile.html index e82f5d58..8ea9ad3c 100644 --- a/app/templates/user/show_profile.html +++ b/app/templates/user/show_profile.html @@ -10,19 +10,19 @@ -

    {{ user.user_name if user.ap_id == none else user.ap_id }}

    +

    {{ user.display_name() if user.is_local() else user.ap_id }}

    {% elif user.avatar_image() != '' %}
    -

    {{ user.user_name if user.ap_id == none else user.ap_id }}

    +

    {{ user.display_name() if user.is_local() else user.ap_id }}

    {% else %} @@ -33,7 +33,7 @@ -

    {{ user.user_name if user.ap_id == none else user.ap_id }}

    +

    {{ user.display_name() if user.is_local() else user.ap_id }}

    {% endif %}

    {{ _('Joined') }}: {{ moment(user.created).fromNow(refresh=True) }}
    {{ _('Attitude') }}: {{ (user.attitude * 100) | round | int }}%

    diff --git a/app/user/forms.py b/app/user/forms.py index ea8b3f4e..632029b8 100644 --- a/app/user/forms.py +++ b/app/user/forms.py @@ -7,6 +7,7 @@ from flask_babel import _, lazy_gettext as _l class ProfileForm(FlaskForm): + title = StringField(_l('Display name'), validators=[Optional(), Length(max=255)]) email = EmailField(_l('Email address'), validators=[Email(), DataRequired(), Length(min=5, max=255)]) password_field = PasswordField(_l('Set new password'), validators=[Optional(), Length(min=1, max=50)], render_kw={"autocomplete": 'Off'}) diff --git a/app/user/routes.py b/app/user/routes.py index 0de8f5a1..b96f8fec 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -68,6 +68,7 @@ def edit_profile(actor): abort(401) form = ProfileForm() if form.validate_on_submit() and not current_user.banned: + current_user.title = form.title.data current_user.email = form.email.data if form.password_field.data.strip() != '': current_user.set_password(form.password_field.data) @@ -77,11 +78,25 @@ def edit_profile(actor): current_user.bot = form.bot.data profile_file = request.files['profile_file'] if profile_file and profile_file.filename != '': + # remove old avatar + file = File.query.get(current_user.avatar_id) + file.delete_from_disk() + current_user.avatar_id = None + db.session.delete(file) + + # add new avatar file = save_icon_file(profile_file, 'users') if file: current_user.avatar = file banner_file = request.files['banner_file'] if banner_file and banner_file.filename != '': + # remove old cover + file = File.query.get(current_user.cover_id) + file.delete_from_disk() + current_user.cover_id = None + db.session.delete(file) + + # add new cover file = save_banner_file(banner_file, 'users') if file: current_user.cover = file @@ -92,6 +107,7 @@ def edit_profile(actor): return redirect(url_for('user.edit_profile', actor=actor)) elif request.method == 'GET': + form.title.data = current_user.title form.email.data = current_user.email form.about.data = current_user.about form.matrix_user_id.data = current_user.matrix_user_id @@ -200,6 +216,7 @@ def delete_profile(actor): else: user.banned = True user.deleted = True + user.delete_dependencies() db.session.commit() flash(f'{actor} has been deleted.') diff --git a/migrations/versions/0dadae40281d_user_title.py b/migrations/versions/0dadae40281d_user_title.py new file mode 100644 index 00000000..723ab523 --- /dev/null +++ b/migrations/versions/0dadae40281d_user_title.py @@ -0,0 +1,32 @@ +"""user title + +Revision ID: 0dadae40281d +Revises: c80716fd7b79 +Create Date: 2024-01-01 14:12:01.062643 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0dadae40281d' +down_revision = 'c80716fd7b79' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('title', sa.String(length=256), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('title') + + # ### end Alembic commands ###