From 1550d9bb70849914b560d94e7eb2c7b8b4553da6 Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Mon, 9 Dec 2024 22:52:38 +0100 Subject: [PATCH 01/26] render user note --- app/models.py | 9 ++++++++- app/templates/base.html | 6 ++++++ app/templates/user/show_profile.html | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index 0e0a410d..bdb596ba 100644 --- a/app/models.py +++ b/app/models.py @@ -1022,6 +1022,7 @@ class User(UserMixin, db.Model): db.session.query(PostBookmark).filter(PostBookmark.user_id == self.id).delete() db.session.query(PostReplyBookmark).filter(PostReplyBookmark.user_id == self.id).delete() db.session.query(ModLog).filter(ModLog.user_id == self.id).delete() + db.session.query(UserNote).filter(or_(UserNote.user_id == self.id, UserNote.target_id == self.id)).delete() def purge_content(self, soft=True): files = File.query.join(Post).filter(Post.user_id == self.id).all() @@ -1077,7 +1078,13 @@ class User(UserMixin, db.Model): # returns true if the post has been read, false if not def has_read_post(self, post): return self.read_post.filter(read_posts.c.read_post_id == post.id).count() > 0 - + + def get_note(self, by_user): + user_note = UserNote.query.filter(UserNote.target_id == self.id, UserNote.user_id == by_user.id).first() + if user_note: + return user_note.body + else: + return None class ActivityLog(db.Model): diff --git a/app/templates/base.html b/app/templates/base.html index 718d1d83..dff76458 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -27,6 +27,12 @@ {% endif -%} {% endif -%} + {% if current_user.is_authenticated -%} + {% set user_note = user.get_note(current_user) %} + {% if user_note -%} + [{{ user_note | truncate(12, True) }}] + {% endif -%} + {% endif -%} {% endif -%} {% endmacro -%} diff --git a/app/templates/user/show_profile.html b/app/templates/user/show_profile.html index 0b76b8da..5ef64f2f 100644 --- a/app/templates/user/show_profile.html +++ b/app/templates/user/show_profile.html @@ -116,6 +116,7 @@ {% if current_user.is_authenticated and current_user.is_admin() and user.reputation %}{{ _('Reputation') }}: {{ user.reputation | round | int }}
{% endif %} {{ _('Posts') }}: {{ user.post_count }}
{{ _('Comments') }}: {{ user.post_reply_count }}
+ {% if current_user.is_authenticated %}{{ _('User note') }}: {{ user.get_note(current_user) }}
{% endif %}

{{ user.about_html|safe }} From 148df4fcbaf193e7aefdac9b205ee51ad44f70eb Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Tue, 10 Dec 2024 09:56:30 +0100 Subject: [PATCH 02/26] edit form for user note --- app/templates/user/edit_note.html | 69 ++++++++++++++++++++++++++++ app/templates/user/show_profile.html | 1 + app/user/forms.py | 5 ++ app/user/routes.py | 38 ++++++++++++++- 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 app/templates/user/edit_note.html diff --git a/app/templates/user/edit_note.html b/app/templates/user/edit_note.html new file mode 100644 index 00000000..6ab886a6 --- /dev/null +++ b/app/templates/user/edit_note.html @@ -0,0 +1,69 @@ +{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') -%} + {% extends 'themes/' + theme() + '/base.html' %} +{% else -%} + {% extends "base.html" %} +{% endif -%} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+ +
+ +{% endblock %} diff --git a/app/templates/user/show_profile.html b/app/templates/user/show_profile.html index 5ef64f2f..f50865dd 100644 --- a/app/templates/user/show_profile.html +++ b/app/templates/user/show_profile.html @@ -100,6 +100,7 @@ {% endif -%} {% endif -%}
  • {{ _('Report') }}
  • +
  • {{ _('Edit note') }}
  • {% endif %} diff --git a/app/user/forms.py b/app/user/forms.py index dc702ad2..e17407ce 100644 --- a/app/user/forms.py +++ b/app/user/forms.py @@ -147,3 +147,8 @@ class RemoteFollowForm(FlaskForm): instance_type = SelectField(_l('Instance type'), choices=type_choices, render_kw={'class': 'form-select'}) submit = SubmitField(_l('View profile on remote instance')) + + +class UserNoteForm(FlaskForm): + note = StringField(_l('User note'), validators=[Optional(), Length(max=50)]) + submit = SubmitField(_l('Save note')) diff --git a/app/user/routes.py b/app/user/routes.py index 5bc30c38..12a6e7a9 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -16,10 +16,10 @@ from app.constants import * from app.email import send_verification_email from app.models import Post, Community, CommunityMember, User, PostReply, PostVote, Notification, utcnow, File, Site, \ Instance, Report, UserBlock, CommunityBan, CommunityJoinRequest, CommunityBlock, Filter, Domain, DomainBlock, \ - InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic + InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic, UserNote from app.user import bp from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm, \ - FilterForm, KeywordFilterEditForm, RemoteFollowForm, ImportExportForm + FilterForm, KeywordFilterEditForm, RemoteFollowForm, ImportExportForm, UserNoteForm from app.user.utils import purge_user_then_delete, unsubscribe_from_community from app.utils import get_setting, render_template, markdown_to_html, user_access, markdown_to_text, shorten_string, \ is_image_url, ensure_directory_exists, gibberish, file_get_contents, community_membership, user_filters_home, \ @@ -1330,3 +1330,37 @@ def user_read_posts_delete(): db.session.commit() flash(_('Reading history has been deleted')) return redirect(url_for('user.user_read_posts')) + + +@bp.route('/u//note', methods=['GET', 'POST']) +@login_required +def edit_user_note(actor): + actor = actor.strip() + if '@' in actor: + user: User = User.query.filter_by(ap_id=actor, deleted=False, banned=False).first() + else: + user: User = User.query.filter_by(user_name=actor, deleted=False, ap_id=None).first() + if user is None: + abort(404) + form = UserNoteForm() + if form.validate_on_submit() and not current_user.banned: + text = form.note.data.strip() + usernote = UserNote.query.filter(UserNote.target_id == user.id, UserNote.user_id == current_user.id).first() + if usernote: + usernote.body = text + else: + usernote = UserNote(target_id=user.id, user_id=current_user.id, body=text) + db.session.add(usernote) + db.session.commit() + + flash(_('Your changes have been saved.'), 'success') + referrer = request.headers.get('Referer', None) + if referrer is not None: + return redirect(referrer) + else: + return redirect(url_for('user.edit_user_note', actor=actor)) + elif request.method == 'GET': + form.note.data = user.get_note(current_user) + + return render_template('user/edit_note.html', title=_('Edit note'), form=form, user=user, + menu_topics=menu_topics(), site=g.site) From b4f1706a6b71f50d93326daf024ff01a65141e9e Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Wed, 11 Dec 2024 16:33:42 +0100 Subject: [PATCH 03/26] user_notes relationship, caching, cleanup --- app/models.py | 4 +++- app/templates/base.html | 2 +- app/templates/user/edit_note.html | 21 ++++++++++----------- app/user/routes.py | 11 +++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/models.py b/app/models.py index bdb596ba..a95d026a 100644 --- a/app/models.py +++ b/app/models.py @@ -710,6 +710,7 @@ class User(UserMixin, db.Model): cover = db.relationship('File', lazy='joined', foreign_keys=[cover_id], single_parent=True, cascade="all, delete-orphan") instance = db.relationship('Instance', lazy='joined', foreign_keys=[instance_id]) conversations = db.relationship('Conversation', lazy='dynamic', secondary=conversation_member, backref=db.backref('members', lazy='joined')) + user_notes = db.relationship('UserNote', lazy='dynamic', foreign_keys="UserNote.target_id") ap_id = db.Column(db.String(255), index=True) # e.g. username@server ap_profile_id = db.Column(db.String(255), index=True, unique=True) # e.g. https://server/u/username @@ -1079,8 +1080,9 @@ class User(UserMixin, db.Model): def has_read_post(self, post): return self.read_post.filter(read_posts.c.read_post_id == post.id).count() > 0 + @cache.memoize(timeout=500) def get_note(self, by_user): - user_note = UserNote.query.filter(UserNote.target_id == self.id, UserNote.user_id == by_user.id).first() + user_note = self.user_notes.filter(UserNote.target_id == self.id, UserNote.user_id == by_user.id).first() if user_note: return user_note.body else: diff --git a/app/templates/base.html b/app/templates/base.html index dff76458..320d8bd5 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -30,7 +30,7 @@ {% if current_user.is_authenticated -%} {% set user_note = user.get_note(current_user) %} {% if user_note -%} - [{{ user_note | truncate(12, True) }}] + [{{ user_note | truncate(12, True) }}] {% endif -%} {% endif -%} {% endif -%} diff --git a/app/templates/user/edit_note.html b/app/templates/user/edit_note.html index 6ab886a6..5f003e0e 100644 --- a/app/templates/user/edit_note.html +++ b/app/templates/user/edit_note.html @@ -12,9 +12,8 @@
    {{ _('Edit note for "%(user_name)s"', user_name=user.display_name()) }}
    + {{ _('Emoji quick access') }}
    - Emoji quick access -
    @@ -23,27 +22,27 @@ -
    +
    + - -
    +
    + + + + + - - - - -
    {{ render_form(form) }} -
    This note appears next to the username. It's just for you and not displayed to anyone else.
    +
    {{ _('This note appears next to their username. It\'s meant just for you and not displayed to anyone else.') }}
    diff --git a/app/user/routes.py b/app/user/routes.py index 12a6e7a9..fbde47f4 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -1337,7 +1337,7 @@ def user_read_posts_delete(): def edit_user_note(actor): actor = actor.strip() if '@' in actor: - user: User = User.query.filter_by(ap_id=actor, deleted=False, banned=False).first() + user: User = User.query.filter_by(ap_id=actor, deleted=False).first() else: user: User = User.query.filter_by(user_name=actor, deleted=False, ap_id=None).first() if user is None: @@ -1352,13 +1352,12 @@ def edit_user_note(actor): usernote = UserNote(target_id=user.id, user_id=current_user.id, body=text) db.session.add(usernote) db.session.commit() + cache.delete_memoized(User.get_note, user, current_user) flash(_('Your changes have been saved.'), 'success') - referrer = request.headers.get('Referer', None) - if referrer is not None: - return redirect(referrer) - else: - return redirect(url_for('user.edit_user_note', actor=actor)) + goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}' + return redirect(goto) + elif request.method == 'GET': form.note.data = user.get_note(current_user) From 410c4735471b4fdac6d37802eb873450bdf449d3 Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Thu, 12 Dec 2024 20:43:20 +0100 Subject: [PATCH 04/26] add modlog to the admin tools --- app/templates/admin/_nav.html | 1 + app/templates/base.html | 3 ++- app/templates/modlog.html | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/templates/admin/_nav.html b/app/templates/admin/_nav.html index e04d0b84..dea6efd2 100644 --- a/app/templates/admin/_nav.html +++ b/app/templates/admin/_nav.html @@ -15,6 +15,7 @@ {{ _('Newsletter') }} | {{ _('Permissions') }} | {{ _('Activities') }} + {{ _('Modlog') }} {% if debug_mode %} | {{ _('Dev Tools') }} {% endif%} diff --git a/app/templates/base.html b/app/templates/base.html index 718d1d83..a7467df1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -228,8 +228,9 @@
  • {{ _('Federation') }}
  • {{ _('Instances') }}
  • {{ _('Newsletter') }}
  • -
  • {{ _('Activities') }}
  • {{ _('Permissions') }}
  • +
  • {{ _('Activities') }}
  • +
  • {{ _('Modlog') }}
  • {% if debug_mode %}
  • {{ _('Dev Tools') }}
  • {% endif %} diff --git a/app/templates/modlog.html b/app/templates/modlog.html index 7c905546..1068f169 100644 --- a/app/templates/modlog.html +++ b/app/templates/modlog.html @@ -4,7 +4,7 @@ {% extends "base.html" -%} {% endif -%} -%} {% from 'bootstrap5/form.html' import render_form -%} -{% set active_child = 'list_communities' -%} +{% set active_child = 'modlog' -%} {% block app_content -%}

    {{ _('Moderation log') }}

    From fb1b35757684fee76387e58568bd71a8e91aa469 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:27:55 +1300 Subject: [PATCH 05/26] add pf_network and depends_on to docker compose file --- compose.yaml | 32 +++++++++++++++++++++++++++++--- entrypoint.sh | 0 entrypoint_celery.sh | 0 3 files changed, 29 insertions(+), 3 deletions(-) mode change 100644 => 100755 entrypoint.sh mode change 100644 => 100755 entrypoint_celery.sh diff --git a/compose.yaml b/compose.yaml index 4e0b63e8..d6a62160 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,5 @@ services: + db: shm_size: 128mb image: postgres @@ -6,37 +7,62 @@ services: - ./.env.docker volumes: - ./pgdata:/var/lib/postgresql/data + networks: + - pf_network + redis: image: redis env_file: - ./.env.docker + networks: + - pf_network + celery: build: context: . target: builder + container_name: piefed_celery1 + depends_on: + - db + - redis env_file: - ./.env.docker entrypoint: ./entrypoint_celery.sh volumes: - ./media/:/app/app/static/media/ - web: + networks: + - pf_network + + web: build: context: . target: builder + container_name: piefed_app1 depends_on: - db - redis env_file: - ./.env.docker volumes: - - ./.gunicorn.conf.py:/app/gunicorn.conf.py + - ./gunicorn.conf.py:/app/gunicorn.conf.py - ./media/:/app/app/static/media/ ports: - - '8080:5000' + - '8030:5000' + networks: + - pf_network + adminer: image: adminer restart: always ports: - 8888:8080 + depends_on: + - db + networks: + - pf_network +networks: + pf_network: + name: pf_network + external: false diff --git a/entrypoint.sh b/entrypoint.sh old mode 100644 new mode 100755 diff --git a/entrypoint_celery.sh b/entrypoint_celery.sh old mode 100644 new mode 100755 From 317e33fc2ab95f152061f0dfde9f773f7d41b0a8 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:30:28 +1300 Subject: [PATCH 06/26] sample docker config --- env.docker.sample | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 env.docker.sample diff --git a/env.docker.sample b/env.docker.sample new file mode 100644 index 00000000..45bd0606 --- /dev/null +++ b/env.docker.sample @@ -0,0 +1,24 @@ +SECRET_KEY='change this to random characters' + +SERVER_NAME='your_domain_here' + +POSTGRES_USER='piefed' +POSTGRES_PASSWORD='piefed' +POSTGRES_DB='piefed' + +DATABASE_URL=postgresql+psycopg2://piefed:piefed@db/piefed + +CACHE_TYPE='RedisCache' +CACHE_REDIS_DB=1 +CACHE_REDIS_URL='redis://redis:6379/0' +CELERY_BROKER_URL='redis://redis:6379/1' + +MODE='production' +FULL_AP_CONTEXT=0 + +SPICY_UNDER_10 = 2.5 +SPICY_UNDER_30 = 1.85 +SPICY_UNDER_60 = 1.25 + + + From d73c12d4c761b95f34c1b7c957aa6c6bda6b852a Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:49:43 +1300 Subject: [PATCH 07/26] remove moviepy and ffmpeg dependency --- Dockerfile | 2 +- app/activitypub/util.py | 217 ++++++++++++++++------------------------ app/models.py | 2 +- app/utils.py | 47 --------- requirements.txt | 1 - 5 files changed, 89 insertions(+), 180 deletions(-) diff --git a/Dockerfile b/Dockerfile index 08a9130c..b10e111b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM --platform=$BUILDPLATFORM python:3-alpine AS builder RUN apk update RUN apk add pkgconfig -RUN apk add --virtual build-deps gcc python3-dev musl-dev tesseract-ocr tesseract-ocr-data-eng ffmpeg +RUN apk add --virtual build-deps gcc python3-dev musl-dev tesseract-ocr tesseract-ocr-data-eng WORKDIR /app COPY . /app diff --git a/app/activitypub/util.py b/app/activitypub/util.py index c4aa0014..2fefb9ef 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -10,7 +10,6 @@ import httpx import redis from flask import current_app, request, g, url_for, json from flask_babel import _ -from requests import JSONDecodeError from sqlalchemy import text, func, desc from sqlalchemy.exc import IntegrityError @@ -29,7 +28,7 @@ import pytesseract from app.utils import get_request, allowlist_html, get_setting, ap_datetime, markdown_to_html, \ is_image_url, domain_from_url, gibberish, ensure_directory_exists, head_request, \ shorten_string, remove_tracking_from_link, \ - microblog_content_to_title, generate_image_from_video_url, is_video_url, \ + microblog_content_to_title, is_video_url, \ notification_subscribers, communities_banned_from, actor_contains_blocked_words, \ html_to_text, add_to_modlog_activitypub, joined_communities, \ moderating_communities, get_task_session, is_video_hosting_site, opengraph_parse @@ -1009,148 +1008,106 @@ def make_image_sizes_async(file_id, thumbnail_width, medium_width, directory, to session = get_task_session() file: File = session.query(File).get(file_id) if file and file.source_url: - # Videos (old code. not invoked because file.source_url won't end .mp4 or .webm) - if file.source_url.endswith('.mp4') or file.source_url.endswith('.webm'): - new_filename = gibberish(15) - - # set up the storage directory - directory = f'app/static/media/{directory}/' + new_filename[0:2] + '/' + new_filename[2:4] - ensure_directory_exists(directory) - - # file path and names to store the resized images on disk - final_place = os.path.join(directory, new_filename + '.jpg') - final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp') - try: - generate_image_from_video_url(file.source_url, final_place) - except Exception as e: - return - - if final_place: - image = Image.open(final_place) - img_width = image.width - - # Resize the image to medium - if medium_width: - if img_width > medium_width: - image.thumbnail((medium_width, medium_width)) - image.save(final_place) - file.file_path = final_place - file.width = image.width - file.height = image.height - - # Resize the image to a thumbnail (webp) - if thumbnail_width: - if img_width > thumbnail_width: - image.thumbnail((thumbnail_width, thumbnail_width)) - image.save(final_place_thumbnail, format="WebP", quality=93) - file.thumbnail_path = final_place_thumbnail - file.thumbnail_width = image.width - file.thumbnail_height = image.height - - session.commit() - - # Images + try: + source_image_response = get_request(file.source_url) + except: + pass else: - try: - source_image_response = get_request(file.source_url) - except: - pass - else: - if source_image_response.status_code == 404 and '/api/v3/image_proxy' in file.source_url: - source_image_response.close() - # Lemmy failed to retrieve the image but we might have better luck. Example source_url: https://slrpnk.net/api/v3/image_proxy?url=https%3A%2F%2Fi.guim.co.uk%2Fimg%2Fmedia%2F24e87cb4d730141848c339b3b862691ca536fb26%2F0_164_3385_2031%2Fmaster%2F3385.jpg%3Fwidth%3D1200%26height%3D630%26quality%3D85%26auto%3Dformat%26fit%3Dcrop%26overlay-align%3Dbottom%252Cleft%26overlay-width%3D100p%26overlay-base64%3DL2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctZGVmYXVsdC5wbmc%26enable%3Dupscale%26s%3D0ec9d25a8cb5db9420471054e26cfa63 - # The un-proxied image url is the query parameter called 'url' - parsed_url = urlparse(file.source_url) - query_params = parse_qs(parsed_url.query) - if 'url' in query_params: - url_value = query_params['url'][0] - source_image_response = get_request(url_value) - else: - source_image_response = None - if source_image_response and source_image_response.status_code == 200: - content_type = source_image_response.headers.get('content-type') - if content_type: - if content_type.startswith('image') or (content_type == 'application/octet-stream' and file.source_url.endswith('.avif')): - source_image = source_image_response.content - source_image_response.close() + if source_image_response.status_code == 404 and '/api/v3/image_proxy' in file.source_url: + source_image_response.close() + # Lemmy failed to retrieve the image but we might have better luck. Example source_url: https://slrpnk.net/api/v3/image_proxy?url=https%3A%2F%2Fi.guim.co.uk%2Fimg%2Fmedia%2F24e87cb4d730141848c339b3b862691ca536fb26%2F0_164_3385_2031%2Fmaster%2F3385.jpg%3Fwidth%3D1200%26height%3D630%26quality%3D85%26auto%3Dformat%26fit%3Dcrop%26overlay-align%3Dbottom%252Cleft%26overlay-width%3D100p%26overlay-base64%3DL2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctZGVmYXVsdC5wbmc%26enable%3Dupscale%26s%3D0ec9d25a8cb5db9420471054e26cfa63 + # The un-proxied image url is the query parameter called 'url' + parsed_url = urlparse(file.source_url) + query_params = parse_qs(parsed_url.query) + if 'url' in query_params: + url_value = query_params['url'][0] + source_image_response = get_request(url_value) + else: + source_image_response = None + if source_image_response and source_image_response.status_code == 200: + content_type = source_image_response.headers.get('content-type') + if content_type: + if content_type.startswith('image') or (content_type == 'application/octet-stream' and file.source_url.endswith('.avif')): + source_image = source_image_response.content + source_image_response.close() - content_type_parts = content_type.split('/') - if content_type_parts: - # content type headers often are just 'image/jpeg' but sometimes 'image/jpeg;charset=utf8' + content_type_parts = content_type.split('/') + if content_type_parts: + # content type headers often are just 'image/jpeg' but sometimes 'image/jpeg;charset=utf8' - # Remove ;charset=whatever - main_part = content_type.split(';')[0] + # Remove ;charset=whatever + main_part = content_type.split(';')[0] - # Split the main part on the '/' character and take the second part - file_ext = '.' + main_part.split('/')[1] - file_ext = file_ext.strip() # just to be sure + # Split the main part on the '/' character and take the second part + file_ext = '.' + main_part.split('/')[1] + file_ext = file_ext.strip() # just to be sure - if file_ext == '.jpeg': - file_ext = '.jpg' - elif file_ext == '.svg+xml': - return # no need to resize SVG images - elif file_ext == '.octet-stream': - file_ext = '.avif' - else: - file_ext = os.path.splitext(file.source_url)[1] - file_ext = file_ext.replace('%3f', '?') # sometimes urls are not decoded properly - if '?' in file_ext: - file_ext = file_ext.split('?')[0] + if file_ext == '.jpeg': + file_ext = '.jpg' + elif file_ext == '.svg+xml': + return # no need to resize SVG images + elif file_ext == '.octet-stream': + file_ext = '.avif' + else: + file_ext = os.path.splitext(file.source_url)[1] + file_ext = file_ext.replace('%3f', '?') # sometimes urls are not decoded properly + if '?' in file_ext: + file_ext = file_ext.split('?')[0] - new_filename = gibberish(15) + new_filename = gibberish(15) - # set up the storage directory - directory = f'app/static/media/{directory}/' + new_filename[0:2] + '/' + new_filename[2:4] - ensure_directory_exists(directory) + # set up the storage directory + directory = f'app/static/media/{directory}/' + new_filename[0:2] + '/' + new_filename[2:4] + ensure_directory_exists(directory) - # file path and names to store the resized images on disk - final_place = os.path.join(directory, new_filename + file_ext) - final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp') + # file path and names to store the resized images on disk + final_place = os.path.join(directory, new_filename + file_ext) + final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp') - if file_ext == '.avif': # this is quite a big plugin so we'll only load it if necessary - import pillow_avif + if file_ext == '.avif': # this is quite a big plugin so we'll only load it if necessary + import pillow_avif - # Load image data into Pillow - Image.MAX_IMAGE_PIXELS = 89478485 - image = Image.open(BytesIO(source_image)) - image = ImageOps.exif_transpose(image) - img_width = image.width - img_height = image.height + # Load image data into Pillow + Image.MAX_IMAGE_PIXELS = 89478485 + image = Image.open(BytesIO(source_image)) + image = ImageOps.exif_transpose(image) + img_width = image.width + img_height = image.height - # Resize the image to medium - if medium_width: - if img_width > medium_width: - image.thumbnail((medium_width, medium_width)) - image.save(final_place) - file.file_path = final_place - file.width = image.width - file.height = image.height + # Resize the image to medium + if medium_width: + if img_width > medium_width: + image.thumbnail((medium_width, medium_width)) + image.save(final_place) + file.file_path = final_place + file.width = image.width + file.height = image.height - # Resize the image to a thumbnail (webp) - if thumbnail_width: - if img_width > thumbnail_width: - image.thumbnail((thumbnail_width, thumbnail_width)) - image.save(final_place_thumbnail, format="WebP", quality=93) - file.thumbnail_path = final_place_thumbnail - file.thumbnail_width = image.width - file.thumbnail_height = image.height + # Resize the image to a thumbnail (webp) + if thumbnail_width: + if img_width > thumbnail_width: + image.thumbnail((thumbnail_width, thumbnail_width)) + image.save(final_place_thumbnail, format="WebP", quality=93) + file.thumbnail_path = final_place_thumbnail + file.thumbnail_width = image.width + file.thumbnail_height = image.height - session.commit() + session.commit() - # Alert regarding fascist meme content - if toxic_community and img_width < 2000: # images > 2000px tend to be real photos instead of 4chan screenshots. - try: - image_text = pytesseract.image_to_string(Image.open(BytesIO(source_image)).convert('L'), timeout=30) - except Exception as e: - image_text = '' - if 'Anonymous' in image_text and ('No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345' - post = Post.query.filter_by(image_id=file.id).first() - notification = Notification(title='Review this', - user_id=1, - author_id=post.user_id, - url=url_for('activitypub.post_ap', post_id=post.id)) - session.add(notification) - session.commit() + # Alert regarding fascist meme content + if toxic_community and img_width < 2000: # images > 2000px tend to be real photos instead of 4chan screenshots. + try: + image_text = pytesseract.image_to_string(Image.open(BytesIO(source_image)).convert('L'), timeout=30) + except Exception as e: + image_text = '' + if 'Anonymous' in image_text and ('No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345' + post = Post.query.filter_by(image_id=file.id).first() + notification = Notification(title='Review this', + user_id=1, + author_id=post.user_id, + url=url_for('activitypub.post_ap', post_id=post.id)) + session.add(notification) + session.commit() def find_reply_parent(in_reply_to: str) -> Tuple[int, int, int]: diff --git a/app/models.py b/app/models.py index ceae4a4a..1c8c3f26 100644 --- a/app/models.py +++ b/app/models.py @@ -1364,7 +1364,7 @@ class Post(db.Model): i += 1 db.session.commit() - if post.image_id: + if post.image_id and not post.type == constants.POST_TYPE_VIDEO: make_image_sizes(post.image_id, 170, 512, 'posts', community.low_quality) # the 512 sized image is for masonry view diff --git a/app/utils.py b/app/utils.py index 14bfdc06..45b50df1 100644 --- a/app/utils.py +++ b/app/utils.py @@ -4,7 +4,6 @@ import bisect import hashlib import mimetypes import random -import tempfile import urllib from collections import defaultdict from datetime import datetime, timedelta, date @@ -13,11 +12,9 @@ from typing import List, Literal, Union import httpx import markdown2 -import math from urllib.parse import urlparse, parse_qs, urlencode from functools import wraps import flask -import requests from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning import warnings import jwt @@ -34,7 +31,6 @@ from wtforms.fields import SelectField, SelectMultipleField from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput from app import db, cache, httpx_client import re -from moviepy.editor import VideoFileClip from PIL import Image, ImageOps from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \ @@ -1109,49 +1105,6 @@ def in_sorted_list(arr, target): return index < len(arr) and arr[index] == target -# Makes a still image from a video url, without downloading the whole video file -def generate_image_from_video_url(video_url, output_path, length=2): - - response = requests.get(video_url, stream=True, timeout=5, - headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0'}) # Imgur requires a user agent - content_type = response.headers.get('Content-Type') - if content_type: - if 'video/mp4' in content_type: - temp_file_extension = '.mp4' - elif 'video/webm' in content_type: - temp_file_extension = '.webm' - else: - raise ValueError("Unsupported video format") - else: - raise ValueError("Content-Type not found in response headers") - - # Generate a random temporary file name - temp_file_name = gibberish(15) + temp_file_extension - temp_file_path = os.path.join(tempfile.gettempdir(), temp_file_name) - - # Write the downloaded data to a temporary file - with open(temp_file_path, 'wb') as f: - for chunk in response.iter_content(chunk_size=4096): - f.write(chunk) - if os.path.getsize(temp_file_path) >= length * 1024 * 1024: - break - - # Generate thumbnail from the temporary file - try: - clip = VideoFileClip(temp_file_path) - except Exception as e: - os.unlink(temp_file_path) - raise e - thumbnail = clip.get_frame(0) - clip.close() - - # Save the image - thumbnail_image = Image.fromarray(thumbnail) - thumbnail_image.save(output_path) - - os.remove(temp_file_path) - - @cache.memoize(timeout=600) def recently_upvoted_posts(user_id) -> List[int]: post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'), diff --git a/requirements.txt b/requirements.txt index 3f0cbefd..d2a5bcca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,4 +32,3 @@ Werkzeug==2.3.3 pytesseract==0.3.10 sentry-sdk==1.40.6 python-slugify==8.0.4 -moviepy==1.0.3 From f2fff4e00e2eacbc23b1d1c241e5e6949ec0072f Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:50:40 +1300 Subject: [PATCH 08/26] added docker instructions --- INSTALL.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index d29e7830..31732fb3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,7 +1,8 @@ # Contents -* [Setup Database](#setup-database) -* [Install Python Libraries](#install-python-libraries) +* [Choose your path - easy way or hard way](#choose-path) +* [Setup Database](#setup-database) +* [Install Python Libraries](#install-python-libraries) * [Install additional requirements](#install-additional-requirements) * [Setup pyfedi](#setup-pyfedi) * [Setup .env file](#setup-env-file) @@ -14,6 +15,110 @@ * [Notes for Windows (WSL2)](#notes-for-windows-wsl2) * [Notes for Pip Package Management](#notes-for-pip-package-management) +
    + +## Do you want this the easy way or the hard way? + +### Easy way: docker + +Docker can be used to create an isolated environment that is separate from the host server and starts from a consistent +configuration. While it is quicker and easier, it's not to everyone's taste. + +* Clone PieFed into a new directory + +```bash +git clone https://codeberg.org/rimu/pyfedi.git +``` + +* Copy suggested docker config + +```bash +cd pyfedi +cp env.docker.sample .env.docker +``` + +* Edit docker environment file + +Open .env.docker in your text editor, set SECRET_KEY to something random and set SERVER_NAME to your domain name, +WITHOUT the https:// at the front. The database login details doesn't really need to be changed because postgres will be +locked away inside it's own docker network that only PieFed can access but if you want to change POSTGRES_PASSWORD go ahead +just be sure to update DATABASE_URL accordingly. + +Check out compose.yaml and see if it is to your liking. Note the port (8030) and volume definitions - they might need to be +tweaked. + +* First startup + +This will take a few minutes. + +```bash +export DOCKER_BUILDKIT=1 +docker-compose up --build +``` + +After a while the gibberish will stop scrolling past. If you see errors let us know at [https://piefed.social/c/piefed_help](https://piefed.social/c/piefed_help). + +* Networking + +You need to somehow to allow client connections from outside to access port 8030 on your server. The details of this is outside the scope +of this article. You could use a nginx reverse proxy, a cloudflare zero trust tunnel, tailscale, whatever. Just make sure it has SSL on +it as PieFed assumes you're making requests that start with https://your-domain. + +Once you have the networking set up, go to https://your-domain in your browser and see if the docker output in your terminal +shows signs of reacting to the request. There will be an error showing up in the console because we haven't done the next step yet. + +* Database initialization + +This must be done once and once only. Doing this will wipe all existing data in your instance so do not do it unless you have a +brand new instance. + +Open a shell inside the PieFed docker container: + +`docker exec -it piefed_app1 sh` + +Inside the container, run the initialization command: + +``` +export FLASK_APP=pyfedi.py +flask init-db +``` + +Among other things this process will get you set up with a username and password. Don't use 'admin' as the user name, script kiddies love that one. + +* The moment of truth + +Go to https://your-domain in your web browser and PieFed should appear. Log in with the username and password from the previous step. + +At this point docker is pretty much Ok so you don't need to see the terminal output as readily. Hit Ctrl + C to close down docker and then run + +```bash +docker-compose up -d +``` + +to have PieFed run in the background. + +* But wait there's more + +Until you set the right environment variables, PieFed won't be able to send email. Check out env.sample for some hints. +When you have a new value to set, add it to .env.docker and then restart docker with: + +``` +docker-compose down && docker-compose up -d +``` + +There are also regular cron jobs that need to be run. Set up cron on the host to run those scripts inside the container - see the Cron +section of this document for details. + +You probably want a Captcha on the registration form - more environment variables. + +CDN, CloudFlare. More environment variables. + +All this is explained in the bare metal guide, below. + +### Hard way: bare metal + +Read on +
    ## Setup Database @@ -77,7 +182,7 @@ sudo apt install tesseract-ocr ## Setup PyFedi -* Clone PyFedi +* Clone PieFed ```bash git clone https://codeberg.org/rimu/pyfedi.git From ad55fc510c6dc093d74657e5c8bef4e6da485dc8 Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 17 Dec 2024 12:50:29 +0000 Subject: [PATCH 09/26] Move code to hide duplicate cross-posts into PR #386 --- app/main/routes.py | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/app/main/routes.py b/app/main/routes.py index ba64cf6e..aebf502c 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -116,39 +116,9 @@ def home_page(sort, view_filter): posts = posts.order_by(desc(Post.last_active)) # Pagination - if view_filter == 'subscribed' and current_user.is_authenticated and sort == 'new': - # use python list instead of DB query - posts = posts.all() - - # exclude extra cross-posts from feed - already_seen = [] - limit = 100 if not low_bandwidth else 50 - #i = -1 # option 1: don't exclude cross-posts - #i = min(limit - 1, len(posts) - 1) # option 2: exclude cross-posts from the first page only - i = min((limit * 10) - 1, len(posts) - 1) # option 3: exclude cross-posts across a 'magic number' of pages - #i = len(posts) - 1 # option 4: exclude all cross-posts ever - while i >= 0: - if not posts[i].cross_posts: - i -= 1 - continue - if posts[i].id in already_seen: - posts.pop(i) - else: - already_seen.extend(posts[i].cross_posts) - i -= 1 - - # paginate manually (can't use paginate()) - start = (page - 1) * limit - end = start + limit - posts = posts[start:end] - next_page = page + 1 if len(posts) == limit else None - previous_page = page - 1 if page != 1 else None - next_url = url_for('main.index', page=next_page, sort=sort, view_filter=view_filter) if next_page else None - prev_url = url_for('main.index', page=previous_page, sort=sort, view_filter=view_filter) if previous_page else None - else: - posts = posts.paginate(page=page, per_page=100 if current_user.is_authenticated and not low_bandwidth else 50, error_out=False) - next_url = url_for('main.index', page=posts.next_num, sort=sort, view_filter=view_filter) if posts.has_next else None - prev_url = url_for('main.index', page=posts.prev_num, sort=sort, view_filter=view_filter) if posts.has_prev and page != 1 else None + posts = posts.paginate(page=page, per_page=100 if current_user.is_authenticated and not low_bandwidth else 50, error_out=False) + next_url = url_for('main.index', page=posts.next_num, sort=sort, view_filter=view_filter) if posts.has_next else None + prev_url = url_for('main.index', page=posts.prev_num, sort=sort, view_filter=view_filter) if posts.has_prev and page != 1 else None # Active Communities active_communities = Community.query.filter_by(banned=False) From b8178b650c781d8a4450c88564ef6e0164660e6a Mon Sep 17 00:00:00 2001 From: hono4kami Date: Tue, 17 Dec 2024 18:15:36 +0700 Subject: [PATCH 10/26] fix: make community search page retain fields Make community search page retain `topic_id`, `language_id`, and `search` Refs: #362 --- app/templates/list_communities.html | 60 +++++++++++++++++++---------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 98416594..edf0675a 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -27,27 +27,45 @@
    - {% if topics -%} -
    Topic: - -
    - {% endif -%} - {% if languages -%} -
    Language: - -
    - {% endif -%} -
    +
    + {% if topics -%} +
    + Topic: + +
    + {% endif -%} + {% if languages -%} +
    + Language: + +
    + {% endif -%} +
    + +
    +
    From 3e91ee34f791c3973b735aa8913def73b1e3dee6 Mon Sep 17 00:00:00 2001 From: hono4kami Date: Tue, 17 Dec 2024 19:58:37 +0700 Subject: [PATCH 11/26] feat: make community search community name sort Refs: #362 --- app/templates/list_communities.html | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index edf0675a..8f2b50f5 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -85,9 +85,18 @@ - {{ _('Community') }} + {{ _('Posts') }} From 61963a43dc354de9362a54b480e48a3484477bf3 Mon Sep 17 00:00:00 2001 From: hono4kami Date: Tue, 17 Dec 2024 20:03:13 +0700 Subject: [PATCH 12/26] feat: make community search retain post_count sort Refs: #362 --- app/templates/list_communities.html | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 8f2b50f5..919edbb4 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -99,9 +99,18 @@ - {{ _('Posts') }} + {{ _('Comments') }} From 8dd3d3de9330fdf3edf41725803e5219ab1b7905 Mon Sep 17 00:00:00 2001 From: hono4kami Date: Tue, 17 Dec 2024 20:06:52 +0700 Subject: [PATCH 13/26] feat: make community search retain sort method Make community search retain post_reply_count sort method. Refs: #362 --- app/templates/list_communities.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 919edbb4..d56119f9 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -116,6 +116,18 @@ {{ _('Comments') }} + {{ _('Active') }} From 8ffd02651ceed261f3716f12fd02918172e31781 Mon Sep 17 00:00:00 2001 From: hono4kami Date: Tue, 17 Dec 2024 20:08:40 +0700 Subject: [PATCH 14/26] chore: remove unused link on table header Refs: #362 --- app/templates/list_communities.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index d56119f9..193bb3e0 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -113,9 +113,6 @@ - {{ _('Comments') }} - - - {{ _('Active') }} + From bad1839c148c0ac9fb1d42b3ca42e5b00dc3bb4a Mon Sep 17 00:00:00 2001 From: hono4kami Date: Tue, 17 Dec 2024 20:13:52 +0700 Subject: [PATCH 16/26] fix: fix title attribute Fix title attribute for the button on post count table column header. Refs: #362 --- app/templates/list_communities.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 65659530..235d06c3 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -105,7 +105,7 @@ hx-include="form[action='/communities']" hx-target="body" hx-push-url="true" - title="{{ _('Sort by name') }}" + title="{{ _('Sort by post count') }}" class="btn" > {{ _('Posts') }} From 2d2e18abf753ad7ba4c287cdbb1e88283f7c3609 Mon Sep 17 00:00:00 2001 From: hono4kami Date: Tue, 17 Dec 2024 20:28:10 +0700 Subject: [PATCH 17/26] fix: fix can only search by All communities Refs: #362 --- app/templates/list_communities.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 235d06c3..cf945314 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -27,7 +27,8 @@
    -
    @@ -86,7 +87,7 @@

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/admin/users_trash.html b/app/templates/admin/users_trash.html index 4eae558c..2257353f 100644 --- a/app/templates/admin/users_trash.html +++ b/app/templates/admin/users_trash.html @@ -27,8 +27,6 @@ Rep Banned Reports - IP - Source Actions {% for user in users.items %} @@ -45,8 +43,6 @@ {% if user.reputation %}R {{ user.reputation | round | int }}{% endif %} {{ 'Banned'|safe if user.banned }} {{ user.reports if user.reports > 0 }} - {{ user.ip_address if user.ip_address }} - {{ user.referrer if user.referrer }} Edit | Delete @@ -74,4 +70,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/user/show_profile.html b/app/templates/user/show_profile.html index 4d0f33cf..28a7304b 100644 --- a/app/templates/user/show_profile.html +++ b/app/templates/user/show_profile.html @@ -108,6 +108,8 @@ {% if user.is_instance_admin() or (user.is_local() and user.is_admin()) %}({{ _('Admin') }}){% endif %}
    {% if user.is_admin() or user.is_staff() %}{{ _('Role permissions') }}: {% if user.is_admin() %}{{ _('Admin') }}{% endif %} {% if user.is_staff() %}{{ _('Staff') }}{% endif %}
    {% endif %} {{ _('Joined') }}: {{ arrow.get(user.created).humanize(locale=locale) }}
    + {% if current_user.is_authenticated and current_user.is_admin() %}{{ _('Referer') }}: {{ user.referrer if user.referrer }}
    {% endif %} + {% if current_user.is_authenticated and current_user.is_admin() %}{{ _('IP and country code') }}: {{ user.ip_address if user.ip_address }}{% if user.ip_address_country %} ({{ user.ip_address_country }}){% endif %}
    {% endif %} {% if current_user.is_authenticated and current_user.is_admin() and user.last_seen %}{{ _('Active') }}: {{ arrow.get(user.last_seen).humanize(locale=locale) }}
    {% endif %} {% if user.bot %} {{ _('Bot Account') }}
    From 5e911b48538deadcc505f7b67343921423533d1e Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Wed, 18 Dec 2024 10:01:45 +0100 Subject: [PATCH 23/26] make admin_users table sortable --- app/admin/routes.py | 19 ++++++-- app/templates/admin/users.html | 84 ++++++++++++++++++++++++++-------- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/app/admin/routes.py b/app/admin/routes.py index 530fa333..ee222409 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1147,21 +1147,30 @@ def admin_users(): page = request.args.get('page', 1, type=int) search = request.args.get('search', '') local_remote = request.args.get('local_remote', '') + sort_by = request.args.get('sort_by', 'last_seen DESC') + last_seen = request.args.get('last_seen', 0, type=int) + + sort_by_btn = request.args.get('sort_by_btn', '') + if sort_by_btn: + return redirect(url_for('admin.admin_users', page=page, search=search, local_remote=local_remote, sort_by=sort_by_btn, last_seen=last_seen)) users = User.query.filter_by(deleted=False) if local_remote == 'local': users = users.filter_by(ap_id=None) - if local_remote == 'remote': + elif local_remote == 'remote': users = users.filter(User.ap_id != None) if search: users = users.filter(User.email.ilike(f"%{search}%")) - users = users.order_by(User.user_name).paginate(page=page, per_page=1000, error_out=False) + if last_seen > 0: + users = users.filter(User.last_seen > utcnow() - timedelta(days=last_seen)) + users = users.order_by(text('"user".' + sort_by)) + users = users.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 + next_url = url_for('admin.admin_users', page=users.next_num, search=search, local_remote=local_remote, sort_by=sort_by, last_seen=last_seen) if users.has_next else None + prev_url = url_for('admin.admin_users', page=users.prev_num, search=search, local_remote=local_remote, sort_by=sort_by, last_seen=last_seen) 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, - local_remote=local_remote, search=search, + local_remote=local_remote, search=search, sort_by=sort_by, last_seen=last_seen, moderating_communities=moderating_communities(current_user.get_id()), joined_communities=joined_communities(current_user.get_id()), menu_topics=menu_topics(), diff --git a/app/templates/admin/users.html b/app/templates/admin/users.html index 3a50a508..89acc5fa 100644 --- a/app/templates/admin/users.html +++ b/app/templates/admin/users.html @@ -11,40 +11,84 @@

    {{ _('Users') }}

    {{ _('Add local user') }} - - - - - + +
    + + +
    +
    + + +
    +
    + + +
    + - - - - + - + + + + {% for user in users.items %} - - - - + + + {% endfor %} From d4ff1f924f0e9b4128e3f9f9ad6d3ca937a79206 Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Wed, 18 Dec 2024 13:11:13 +0100 Subject: [PATCH 24/26] remove admin_users_trash in favor of the new admin_users sorting capability --- app/admin/routes.py | 39 --------------- app/templates/admin/_nav.html | 1 - app/templates/admin/users_trash.html | 73 ---------------------------- app/templates/base.html | 1 - 4 files changed, 114 deletions(-) delete mode 100644 app/templates/admin/users_trash.html diff --git a/app/admin/routes.py b/app/admin/routes.py index ee222409..579dc19d 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1178,45 +1178,6 @@ def admin_users(): ) -@bp.route('/users/trash', methods=['GET']) -@login_required -@permission_required('administer all users') -def admin_users_trash(): - - page = request.args.get('page', 1, type=int) - search = request.args.get('search', '') - local_remote = request.args.get('local_remote', '') - type = request.args.get('type', 'bad_rep') - - users = User.query.filter_by(deleted=False) - if local_remote == 'local': - users = users.filter_by(ap_id=None) - if local_remote == 'remote': - users = users.filter(User.ap_id != None) - if search: - users = users.filter(User.email.ilike(f"%{search}%")) - - if type == '' or type == 'bad_rep': - users = users.filter(User.last_seen > utcnow() - timedelta(days=7)) - users = users.filter(User.reputation < -10) - users = users.order_by(User.reputation).paginate(page=page, per_page=1000, error_out=False) - elif type == 'bad_attitude': - users = users.filter(User.last_seen > utcnow() - timedelta(days=7)) - users = users.filter(User.attitude < 0.0).filter(User.reputation < -10) - users = users.order_by(User.attitude).paginate(page=page, per_page=1000, error_out=False) - - next_url = url_for('admin.admin_users_trash', page=users.next_num, search=search, local_remote=local_remote, type=type) if users.has_next else None - prev_url = url_for('admin.admin_users_trash', page=users.prev_num, search=search, local_remote=local_remote, type=type) if users.has_prev and page != 1 else None - - return render_template('admin/users_trash.html', title=_('Problematic users'), next_url=next_url, prev_url=prev_url, users=users, - local_remote=local_remote, search=search, type=type, - moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()), - menu_topics=menu_topics(), - site=g.site - ) - - @bp.route('/content/trash', methods=['GET']) @login_required @permission_required('administer all users') diff --git a/app/templates/admin/_nav.html b/app/templates/admin/_nav.html index dea6efd2..addf534b 100644 --- a/app/templates/admin/_nav.html +++ b/app/templates/admin/_nav.html @@ -6,7 +6,6 @@ {{ _('Communities') }} | {{ _('Topics') }} | {{ _('Users') }} | - {{ _('Watch') }} | {% if site.registration_mode == 'RequireApplication' %} {{ _('Registration applications') }} | {% endif %} diff --git a/app/templates/admin/users_trash.html b/app/templates/admin/users_trash.html deleted file mode 100644 index 2257353f..00000000 --- a/app/templates/admin/users_trash.html +++ /dev/null @@ -1,73 +0,0 @@ -{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %} - {% extends 'themes/' + theme() + '/base.html' %} -{% else %} - {% extends "base.html" %} -{% endif %} -{% from 'bootstrap/form.html' import render_form %} -{% set active_child = 'admin_users_trash' %} - -{% block app_content %} -
    -
    -

    {{ _('Users') }}

    - {{ _('Add local user') }} -
    - - - - - - - -
    {{ _('Name') }}{{ _('Seen') }}{{ _('Attitude') }}{{ _('Reputation') }} + + {{ _('Banned') }}{{ _('Reports') }} + + + + + + + + {{ _('Actions') }}
    {{ render_username(user, add_domain=False) }}
    {{ user.user_name }}{% if not user.is_local() %}@{{ user.ap_domain }}{% endif %}
    {% if request.args.get('local_remote', '') == 'local' %} - {{ arrow.get(user.last_seen).humanize(locale=locale) }} - {% else %} - {{ user.last_seen }} - {% endif %} - {% if user.attitude != 1 %}{{ (user.attitude * 100) | round | int }}%{% endif %}{% if user.reputation %}R {{ user.reputation | round | int }}{% endif %} {{ 'Banned'|safe if user.banned }} {{ 'Banned posts'|safe if user.ban_posts }} {{ 'Banned comments'|safe if user.ban_comments }} {{ user.reports if user.reports > 0 }} Edit | - Delete + {% if user.attitude != 1 %}{{ (user.attitude * 100) | round | int }}%{% endif %}{% if user.reputation %}R {{ user.reputation | round | int }}{% endif %}{{ arrow.get(user.last_seen).humanize(locale=locale) }}Edit, + Delete, +
    + {% if user.banned %} + Ban, + {% else %} + Ban, + {% endif %} + Purge
    - - - - - - - - - - {% for user in users.items %} - - - - - - - - - - {% endfor %} -
    NameSeenAttitudeRepBannedReportsActions
    {{ render_username(user, add_domain=False) }}
    - {{ user.user_name }}{% if not user.is_local() %}@{{ user.ap_domain }}{% endif %}
    {% if request.args.get('local_remote', '') == 'local' %} - {{ arrow.get(user.last_seen).humanize(locale=locale) }} - {% else %} - {{ user.last_seen }} - {% endif %} - {% if user.attitude != 1 %}{{ (user.attitude * 100) | round | int }}%{% endif %}{% if user.reputation %}R {{ user.reputation | round | int }}{% endif %}{{ 'Banned'|safe if user.banned }} {{ user.reports if user.reports > 0 }} Edit | - Delete -
    - -
    - -
    -
    -
    - {% include 'admin/_nav.html' %} -
    -
    -
    -{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index a7467df1..ab2ad7b1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -217,7 +217,6 @@
  • {{ _('Communities') }}
  • {{ _('Topics') }}
  • {{ _('Users') }}
  • -
  • {{ _('Monitoring - users') }}
  • {{ _('Monitoring - content') }}
  • {{ _('Monitoring - spammy content') }}
  • {{ _('Deleted content') }}
  • From dce17fd09467e5b3724e048205db6e1945a5369f Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:21:03 +1300 Subject: [PATCH 25/26] fix where clicking on teaser body text goes to --- app/static/js/scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/js/scripts.js b/app/static/js/scripts.js index ae9bac26..94ffab99 100644 --- a/app/static/js/scripts.js +++ b/app/static/js/scripts.js @@ -39,7 +39,7 @@ document.addEventListener("DOMContentLoaded", function () { function setupPostTeaserHandler() { document.querySelectorAll('.post_teaser_clickable').forEach(div => { div.onclick = function() { - const firstAnchor = this.parentElement.querySelector('a'); + const firstAnchor = this.parentElement.querySelector('h3 a'); if (firstAnchor) { window.location.href = firstAnchor.href; } From 0bdac9f5984ab68b8f4d3e361fa59b393cc5d5f9 Mon Sep 17 00:00:00 2001 From: Hendrik L Date: Thu, 19 Dec 2024 13:06:19 +0100 Subject: [PATCH 26/26] add link to https://join.piefed.social/try/ on closed registration --- app/templates/auth/register.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html index 49673e1e..542f2567 100644 --- a/app/templates/auth/register.html +++ b/app/templates/auth/register.html @@ -19,7 +19,9 @@
    {{ _('Create new account') }}
    {{ render_form(form) }} {% else %} - {{ _('Registration is closed. Only admins can create accounts.') }} +

    {{ _('Registration is closed. Only admins can create accounts.') }}

    +

    {{ _('If you would like to sign up for PieFed, choose one of the other instances in our network:') }}

    +

    {{ _('Try PieFed') }}

    {% endif %}