From 2f15fd5aea4f6415768ad9ea5e3e1e90f8215b37 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Tue, 20 Aug 2024 07:03:08 +1200 Subject: [PATCH] alt user names --- app/activitypub/routes.py | 43 +++++++++++-------- app/auth/routes.py | 7 +-- app/models.py | 14 +++++- app/post/routes.py | 18 ++++---- app/templates/base.html | 1 - app/templates/errors/alt_profile.html | 6 +++ app/templates/user/edit_settings.html | 1 + app/user/forms.py | 1 + app/user/routes.py | 7 +++ .../versions/2cae414cbc7a_alt_user_names.py | 34 +++++++++++++++ 10 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 app/templates/errors/alt_profile.html create mode 100644 migrations/versions/2cae414cbc7a_alt_user_names.py diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index ab409452..a592dd2c 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -1,10 +1,11 @@ -from urllib.parse import urlparse, parse_qs +from flask import request, current_app, abort, jsonify, json, g, url_for, redirect, make_response from flask_login import current_user +from sqlalchemy import desc, or_ +import werkzeug.exceptions from app import db, constants, cache, celery from app.activitypub import bp -from flask import request, current_app, abort, jsonify, json, g, url_for, redirect, make_response from app.activitypub.signature import HttpSignature, post_request, VerificationError, default_context from app.community.routes import show_community @@ -27,8 +28,6 @@ from app.utils import gibberish, get_setting, is_image_url, allowlist_html, rend domain_from_url, markdown_to_html, community_membership, ap_datetime, ip_address, can_downvote, \ can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \ community_moderators, lemmy_markdown_to_html -from sqlalchemy import desc -import werkzeug.exceptions @bp.route('/testredis') @@ -77,7 +76,7 @@ def webfinger(): seperator = 'u' type = 'Person' - user = User.query.filter_by(user_name=actor.strip(), deleted=False, banned=False, ap_id=None).first() + user = User.query.filter(or_(User.user_name == actor.strip(), User.alt_user_name == actor.strip())).filter_by(deleted=False, banned=False, ap_id=None).first() if user is None: community = Community.query.filter_by(name=actor.strip(), ap_id=None).first() if community is None: @@ -233,18 +232,21 @@ def user_profile(actor): if '@' in actor: user: User = User.query.filter_by(ap_id=actor.lower()).first() else: - user: User = User.query.filter_by(user_name=actor, ap_id=None).first() + user: User = User.query.filter(or_(User.user_name == actor, User.alt_user_name == actor)).filter_by(ap_id=None).first() if user is None: user = User.query.filter_by(ap_profile_id=f'https://{current_app.config["SERVER_NAME"]}/u/{actor.lower()}', deleted=False, ap_id=None).first() else: if '@' in actor: user: User = User.query.filter_by(ap_id=actor.lower(), deleted=False, banned=False).first() else: - user: User = User.query.filter_by(user_name=actor, deleted=False, ap_id=None).first() + user: User = User.query.filter(or_(User.user_name == actor, User.alt_user_name == actor)).filter_by(deleted=False, ap_id=None).first() if user is None: user = User.query.filter_by(ap_profile_id=f'https://{current_app.config["SERVER_NAME"]}/u/{actor.lower()}', deleted=False, ap_id=None).first() if user is not None: + main_user_name = True + if user.alt_user_name == actor: + main_user_name = False if request.method == 'HEAD': if is_activitypub_request(): resp = jsonify('') @@ -256,43 +258,48 @@ def user_profile(actor): server = current_app.config['SERVER_NAME'] actor_data = { "@context": default_context(), "type": "Person" if not user.bot else "Service", - "id": user.public_url(), + "id": user.public_url(main_user_name), "preferredUsername": actor, "name": user.title if user.title else user.user_name, - "inbox": f"{user.public_url()}/inbox", - "outbox": f"{user.public_url()}/outbox", + "inbox": f"{user.public_url(main_user_name)}/inbox", + "outbox": f"{user.public_url(main_user_name)}/outbox", "discoverable": user.searchable, "indexable": user.indexable, "manuallyApprovesFollowers": False if not user.ap_manually_approves_followers else user.ap_manually_approves_followers, "publicKey": { - "id": f"{user.public_url()}#main-key", - "owner": user.public_url(), - "publicKeyPem": user.public_key # .replace("\n", "\\n") #LOOKSWRONG + "id": f"{user.public_url(main_user_name)}#main-key", + "owner": user.public_url(main_user_name), + "publicKeyPem": user.public_key }, "endpoints": { "sharedInbox": f"https://{server}/inbox" }, "published": ap_datetime(user.created), } - if user.avatar_id is not None: + if not main_user_name: + actor_data['name'] = 'Anonymous' + if user.avatar_id is not None and main_user_name: actor_data["icon"] = { "type": "Image", "url": f"https://{current_app.config['SERVER_NAME']}{user.avatar_image()}" } - if user.cover_id is not None: + if user.cover_id is not None and main_user_name: actor_data["image"] = { "type": "Image", "url": f"https://{current_app.config['SERVER_NAME']}{user.cover_image()}" } - if user.about_html: + if user.about_html and main_user_name: actor_data['summary'] = user.about_html - if user.matrix_user_id: + if user.matrix_user_id and main_user_name: actor_data['matrixUserId'] = user.matrix_user_id resp = jsonify(actor_data) resp.content_type = 'application/activity+json' return resp else: - return show_profile(user) + if main_user_name: + return show_profile(user) + else: + return render_template('errors/alt_profile.html') else: abort(404) diff --git a/app/auth/routes.py b/app/auth/routes.py index 2d52a683..b3c3651a 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -1,4 +1,5 @@ -from datetime import date, datetime, timedelta +from datetime import date, datetime +from random import randint from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, g from werkzeug.urls import url_parse from flask_login import login_user, logout_user, current_user @@ -12,7 +13,7 @@ from app.auth.util import random_token, normalize_utf, ip2location from app.email import send_verification_email, send_password_reset_email from app.models import User, utcnow, IpBan, UserRegistration, Notification, Site from app.utils import render_template, ip_address, user_ip_banned, user_cookie_banned, banned_ip_addresses, \ - finalize_user_setup, blocked_referrers + finalize_user_setup, blocked_referrers, gibberish @bp.route('/login', methods=['GET', 'POST']) @@ -123,7 +124,7 @@ def register(): user = User(user_name=form.user_name.data, title=form.user_name.data, email=form.real_email.data, verification_token=verification_token, instance_id=1, ip_address=ip_address(), banned=user_ip_banned() or user_cookie_banned(), email_unread_sent=False, - referrer=session.get('Referer', '')) + referrer=session.get('Referer', ''), alt_user_name=gibberish(randint(8, 20))) user.set_password(form.password.data) ip_address_info = ip2location(user.ip_address) user.ip_address_country = ip_address_info['country'] if ip_address_info else '' diff --git a/app/models.py b/app/models.py index d76a4920..a88a8dd0 100644 --- a/app/models.py +++ b/app/models.py @@ -81,6 +81,9 @@ class Instance(db.Model): role = InstanceRole.query.filter_by(instance_id=self.id, user_id=user_id).first() return role and role.role == 'admin' + def votes_are_public(self): + return self.software.lower() == 'lemmy' or self.software.lower() == 'mbin' or self.software.lower() == 'kbin' + def __repr__(self): return ''.format(self.domain) @@ -585,6 +588,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) + alt_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)) @@ -734,6 +738,9 @@ class User(UserMixin, db.Model): size += self.cover.filesize() return size + def vote_privately(self): + return self.alt_user_name is not None and self.alt_user_name != '' + def num_content(self): content = 0 content += db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE user_id = :user_id'), {'user_id': self.id}).scalar() @@ -873,8 +880,11 @@ class User(UserMixin, db.Model): result = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name.lower()}" return result - def public_url(self): - result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}" + def public_url(self, main_user_name=True): + if main_user_name: + result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}" + else: + result = f"https://{current_app.config['SERVER_NAME']}/u/{self.alt_user_name}" return result def created_recently(self): diff --git a/app/post/routes.py b/app/post/routes.py index 88b12548..1748b5b0 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -397,12 +397,12 @@ def post_vote(post_id: int, vote_direction): if not post.community.local_only: if undo: action_json = { - 'actor': current_user.public_url(), + 'actor': current_user.public_url(not(post.community.instance.votes_are_public() and current_user.vote_privately())), 'type': 'Undo', 'id': f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}", 'audience': post.community.public_url(), 'object': { - 'actor': current_user.public_url(), + 'actor': current_user.public_url(not(post.community.instance.votes_are_public() and current_user.vote_privately())), 'object': post.public_url(), 'type': undo, 'id': f"https://{current_app.config['SERVER_NAME']}/activities/{undo.lower()}/{gibberish(15)}", @@ -412,7 +412,7 @@ def post_vote(post_id: int, vote_direction): else: action_type = 'Like' if vote_direction == 'upvote' else 'Dislike' action_json = { - 'actor': current_user.public_url(), + 'actor': current_user.public_url(not(post.community.instance.votes_are_public() and current_user.vote_privately())), 'object': post.profile_id(), 'type': action_type, 'id': f"https://{current_app.config['SERVER_NAME']}/activities/{action_type.lower()}/{gibberish(15)}", @@ -437,7 +437,7 @@ def post_vote(post_id: int, vote_direction): send_to_remote_instance(instance.id, post.community.id, announce) else: success = post_request_in_background(post.community.ap_inbox_url, action_json, current_user.private_key, - current_user.public_url() + '#main-key') + current_user.public_url(not(post.community.instance.votes_are_public() and current_user.vote_privately())) + '#main-key') if not success: flash('Failed to send vote', 'warning') @@ -514,7 +514,7 @@ def comment_vote(comment_id, vote_direction): 'id': f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}", 'audience': comment.community.public_url(), 'object': { - 'actor': current_user.public_url(), + 'actor': current_user.public_url(not(comment.community.instance.votes_are_public() and current_user.vote_privately())), 'object': comment.public_url(), 'type': undo, 'id': f"https://{current_app.config['SERVER_NAME']}/activities/{undo.lower()}/{gibberish(15)}", @@ -524,7 +524,7 @@ def comment_vote(comment_id, vote_direction): else: action_type = 'Like' if vote_direction == 'upvote' else 'Dislike' action_json = { - 'actor': current_user.public_url(), + 'actor': current_user.public_url(not(comment.community.instance.votes_are_public() and current_user.vote_privately())), 'object': comment.public_url(), 'type': action_type, 'id': f"https://{current_app.config['SERVER_NAME']}/activities/{action_type.lower()}/{gibberish(15)}", @@ -548,10 +548,8 @@ def comment_vote(comment_id, vote_direction): if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): send_to_remote_instance(instance.id, comment.community.id, announce) else: - success = post_request_in_background(comment.community.ap_inbox_url, action_json, current_user.private_key, - current_user.public_url() + '#main-key') - if not success: - flash('Failed to send vote', 'warning') + post_request_in_background(comment.community.ap_inbox_url, action_json, current_user.private_key, + current_user.public_url(not(comment.community.instance.votes_are_public() and current_user.vote_privately())) + '#main-key') current_user.last_seen = utcnow() current_user.ip_address = ip_address() diff --git a/app/templates/base.html b/app/templates/base.html index 58a4e3df..53ac4b30 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -114,7 +114,6 @@ setTheme(getPreferredTheme()); - diff --git a/app/templates/errors/alt_profile.html b/app/templates/errors/alt_profile.html new file mode 100644 index 00000000..7317324d --- /dev/null +++ b/app/templates/errors/alt_profile.html @@ -0,0 +1,6 @@ + + +

{{ _('In PieFed, accounts have a main profile and an alternative profile (used for private voting). You are viewing the alternative profile of an account.') }}

+

{{ _('More about this') }}

+ + \ No newline at end of file diff --git a/app/templates/user/edit_settings.html b/app/templates/user/edit_settings.html index a3af581c..c09748fa 100644 --- a/app/templates/user/edit_settings.html +++ b/app/templates/user/edit_settings.html @@ -33,6 +33,7 @@ {{ render_field(form.default_sort) }} {{ render_field(form.default_filter) }} {{ render_field(form.theme) }} + {{ render_field(form.vote_privately) }}
Import
{{ render_field(form.import_file) }} {{ render_field(form.submit) }} diff --git a/app/user/forms.py b/app/user/forms.py index 73b76d38..6dd64f7a 100644 --- a/app/user/forms.py +++ b/app/user/forms.py @@ -44,6 +44,7 @@ class SettingsForm(FlaskForm): searchable = BooleanField(_l('Show profile in user list')) indexable = BooleanField(_l('My posts appear in search results')) manually_approves_followers = BooleanField(_l('Manually approve followers')) + vote_privately = BooleanField(_l('Vote privately')) import_file = FileField(_l('Import community subscriptions and user blocks from Lemmy')) sorts = [('hot', _l('Hot')), ('top', _l('Top')), diff --git a/app/user/routes.py b/app/user/routes.py index 77d3ea62..afbc41ef 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta from time import sleep +from random import randint from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort, json, g from flask_login import login_user, logout_user, current_user, login_required @@ -238,6 +239,11 @@ def change_settings(): current_user.markdown_editor = form.markdown_editor.data current_user.interface_language = form.interface_language.data session['ui_language'] = form.interface_language.data + if form.vote_privately.data: + if current_user.alt_user_name is None or current_user.alt_user_name == '': + current_user.alt_user_name = gibberish(randint(8, 20)) + else: + current_user.alt_user_name = '' import_file = request.files['import_file'] if propagate_indexable: db.session.execute(text('UPDATE "post" set indexable = :indexable WHERE user_id = :user_id'), @@ -274,6 +280,7 @@ def change_settings(): form.theme.data = current_user.theme form.markdown_editor.data = current_user.markdown_editor form.interface_language.data = current_user.interface_language + form.vote_privately.data = current_user.vote_privately() return render_template('user/edit_settings.html', title=_('Edit profile'), form=form, user=current_user, moderating_communities=moderating_communities(current_user.get_id()), diff --git a/migrations/versions/2cae414cbc7a_alt_user_names.py b/migrations/versions/2cae414cbc7a_alt_user_names.py new file mode 100644 index 00000000..e85ab6ed --- /dev/null +++ b/migrations/versions/2cae414cbc7a_alt_user_names.py @@ -0,0 +1,34 @@ +"""alt user names + +Revision ID: 2cae414cbc7a +Revises: f1f38dabd541 +Create Date: 2024-08-19 19:38:36.616993 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2cae414cbc7a' +down_revision = 'f1f38dabd541' +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('alt_user_name', sa.String(length=255), nullable=True)) + batch_op.create_index(batch_op.f('ix_user_alt_user_name'), ['alt_user_name'], unique=False) + + # ### 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_index(batch_op.f('ix_user_alt_user_name')) + batch_op.drop_column('alt_user_name') + + # ### end Alembic commands ###