diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 00431389..1873e7aa 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -300,6 +300,12 @@ def user_profile(actor): actor_data['source'] = {'content': user.about, 'mediaType': 'text/markdown'} if user.matrix_user_id and main_user_name: actor_data['matrixUserId'] = user.matrix_user_id + if user.extra_fields.count() > 0: + actor_data['attachment'] = [] + for field in user.extra_fields: + actor_data['attachment'].append({'type': 'PropertyValue', + 'name': field.label, + 'value': field.text}) resp = jsonify(actor_data) resp.content_type = 'application/activity+json' resp.headers.set('Link', f'; rel="alternate"; type="text/html"') diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 2fefb9ef..e7da30dc 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -16,7 +16,8 @@ from sqlalchemy.exc import IntegrityError from app import db, cache, constants, celery from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \ PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole, Report, Conversation, \ - Language, Tag, Poll, PollChoice, UserFollower, CommunityBan, CommunityJoinRequest, NotificationSubscription, Licence + Language, Tag, Poll, PollChoice, UserFollower, CommunityBan, CommunityJoinRequest, NotificationSubscription, \ + Licence, UserExtraField from app.activitypub.signature import signed_get_request, post_request import time from app.constants import * @@ -522,6 +523,11 @@ def refresh_user_profile_task(user_id): user.about_html = markdown_to_html(user.about) # prefer Markdown if provided, overwrite version obtained from HTML else: user.about = html_to_text(user.about_html) + if 'attachment' in activity_json and isinstance(activity_json['attachment'], list): + user.extra_fields = [] + for field_data in activity_json['attachment']: + if field_data['type'] == 'PropertyValue': + user.extra_fields.append(UserExtraField(label=field_data['name'].strip(), text=field_data['value'].strip())) if 'type' in activity_json: user.bot = True if activity_json['type'] == 'Service' else False user.ap_fetched_at = utcnow() @@ -769,6 +775,11 @@ def actor_json_to_model(activity_json, address, server): cover = File(source_url=activity_json['image']['url']) user.cover = cover db.session.add(cover) + if 'attachment' in activity_json and isinstance(activity_json['attachment'], list): + user.extra_fields = [] + for field_data in activity_json['attachment']: + if field_data['type'] == 'PropertyValue': + user.extra_fields.append(UserExtraField(label=field_data['name'].strip(), text=field_data['value'].strip())) try: db.session.add(user) db.session.commit() diff --git a/app/cli.py b/app/cli.py index dee895fb..9290ef6e 100644 --- a/app/cli.py +++ b/app/cli.py @@ -27,7 +27,7 @@ from app.models import Settings, BannedInstances, Interest, Role, User, RolePerm from app.post.routes import post_delete_post from app.utils import file_get_contents, retrieve_block_list, blocked_domains, retrieve_peertube_block_list, \ shorten_string, get_request, html_to_text, blocked_communities, ap_datetime, gibberish, get_request_instance, \ - instance_banned + instance_banned, recently_upvoted_post_replies, recently_upvoted_posts, jaccard_similarity def register(app): @@ -464,6 +464,26 @@ def register(app): db.session.query(ActivityPubLog).filter(ActivityPubLog.created_at < utcnow() - timedelta(days=3)).delete() db.session.commit() + @app.cli.command("detect_vote_manipulation") + def detect_vote_manipulation(): + with app.app_context(): + print('Getting user ids...') + all_user_ids = [user.id for user in User.query.filter(User.last_seen > datetime.utcnow() - timedelta(days=7))] + print('Checking...') + for i, first_user_id in enumerate(all_user_ids): + current_user_upvoted_posts = ['post/' + str(id) for id in recently_upvoted_posts(first_user_id)] + current_user_upvoted_replies = ['reply/' + str(id) for id in recently_upvoted_post_replies(first_user_id)] + + current_user_upvotes = set(current_user_upvoted_posts + current_user_upvoted_replies) + if len(current_user_upvotes) > 12: + print(i) + for j in range(i + 1, len(all_user_ids)): + other_user_id = all_user_ids[j] + if jaccard_similarity(current_user_upvotes, other_user_id) >= 95: + first_user = User.query.get(first_user_id) + other_user = User.query.get(other_user_id) + print(f'{first_user.link()} votes the same as {other_user.link()}') + @app.cli.command("migrate_community_notifs") def migrate_community_notifs(): with app.app_context(): diff --git a/app/models.py b/app/models.py index 371606ad..22c28286 100644 --- a/app/models.py +++ b/app/models.py @@ -726,6 +726,7 @@ class User(UserMixin, db.Model): activity = db.relationship('ActivityLog', backref='account', lazy='dynamic', cascade="all, delete-orphan") posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan") post_replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan") + extra_fields = db.relationship('UserExtraField', lazy='dynamic', cascade="all, delete-orphan") roles = db.relationship('Role', secondary=user_role, lazy='dynamic', cascade="all, delete") @@ -2056,6 +2057,13 @@ class UserNote(db.Model): created_at = db.Column(db.DateTime, default=utcnow) +class UserExtraField(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) + label = db.Column(db.String(50)) + text = db.Column(db.String(256)) + + class UserBlock(db.Model): id = db.Column(db.Integer, primary_key=True) blocker_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) diff --git a/app/post/routes.py b/app/post/routes.py index 61f43881..453504e5 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -680,7 +680,7 @@ def add_reply(post_id: int, comment_id: int): inoculation=inoculation[randint(0, len(inoculation) - 1)] if g.site.show_inoculation_block else None) -@bp.route('/post//options', methods=['GET']) +@bp.route('/post//options_menu', methods=['GET']) def post_options(post_id: int): post = Post.query.get_or_404(post_id) if post.deleted: @@ -701,7 +701,7 @@ def post_options(post_id: int): menu_topics=menu_topics(), site=g.site) -@bp.route('/post//comment//options', methods=['GET']) +@bp.route('/post//comment//options_menu', methods=['GET']) def post_reply_options(post_id: int, comment_id: int): post = Post.query.get_or_404(post_id) post_reply = PostReply.query.get_or_404(comment_id) diff --git a/app/static/styles.css b/app/static/styles.css index 9697cc5b..91d6b8cd 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -981,6 +981,13 @@ time { height: 44px; line-height: 44px; } +.post_utilities_bar div .dropdown-item { + white-space: unset; + line-height: 30px; +} +.post_utilities_bar div .dropdown-item:active { + line-height: 26px; +} .post_utilities_bar .notify_toggle { margin-left: auto; /* pull right */ } @@ -1343,7 +1350,7 @@ time { display: inline; } } -.comment .comment_author img { +.comment .comment_author .author_link img { width: 25px; height: 25px; border-radius: 50%; @@ -1384,12 +1391,23 @@ time { margin-left: auto; /* pull right */ font-size: 87%; } +.comment .comment_actions .dropdown-item { + white-space: unset; + line-height: 30px; +} +.comment .comment_actions .dropdown-item:active { + line-height: 26px; +} .comment .replies { margin-top: 0; border-left: solid 1px #ddd; border-top: solid 1px #ddd; } +.hide-labels label { + display: none; +} + #add_local_community_form #url { width: 297px; display: inline-block; @@ -1480,10 +1498,6 @@ fieldset legend { overflow-x: auto; } -.list-group-item:first-child { - padding-top: 0; -} - .skip-link:focus { top: 0; } @@ -1701,6 +1715,9 @@ h1 .warning_badge { .side_pane img { max-width: 100%; } +.side_pane .list-group-item:first-child { + padding-top: 0; +} [data-bs-theme=dark] .main_pane { border-color: #424549; @@ -1942,6 +1959,32 @@ form h5 { padding-top: 0.12rem !important; } +.render_username { + position: relative; +} +.render_username .author_link { + display: inline-block; +} +@media (min-width: 1280px) { + .render_username .author_link:hover + .user_preview, .render_username .user_preview:hover { + display: inline-block !important; + position: absolute; + top: 17px; + left: 0; + background-color: white; + z-index: 20; + } + .render_username .user_preview .card { + width: 300px; + } + .render_username .user_preview .card .preview_avatar_image { + max-width: 50%; + } + .render_username .user_preview .card .preview_avatar_image img { + max-width: 100%; + } +} + /* high contrast */ @media (prefers-contrast: more) { :root { diff --git a/app/static/styles.scss b/app/static/styles.scss index 6c4b53f9..f2fa6014 100644 --- a/app/static/styles.scss +++ b/app/static/styles.scss @@ -581,6 +581,15 @@ time { min-width: $min-touch-target; height: $min-touch-target; line-height: $min-touch-target; + + .dropdown-item { + white-space: unset; + line-height: 30px; + + &:active { + line-height: 26px; + } + } } .notify_toggle { @@ -1001,7 +1010,7 @@ time { } } - .comment_author { + .comment_author .author_link { img { width: 25px; height: 25px; @@ -1046,6 +1055,15 @@ time { margin-left: auto; /* pull right */ font-size: 87%; } + + .dropdown-item { + white-space: unset; + line-height: 30px; + + &:active { + line-height: 26px; + } + } } .replies { @@ -1055,6 +1073,10 @@ time { } } +.hide-labels label { + display: none; +} + #add_local_community_form { #url { width: 297px; @@ -1148,10 +1170,6 @@ fieldset { } } -.list-group-item:first-child { - padding-top: 0; -} - .skip-link:focus { top: 0; } @@ -1388,6 +1406,10 @@ h1 .warning_badge { img { max-width: 100%; } + + .list-group-item:first-child { + padding-top: 0; + } } [data-bs-theme=dark] .main_pane { @@ -1653,6 +1675,37 @@ form { padding-top: .12rem !important; } +.render_username { + position: relative; + + .author_link { + display: inline-block; + } + + @include breakpoint(laptop) { + .author_link:hover + .user_preview, .user_preview:hover { + display: inline-block !important; + position: absolute; + top: 17px; + left: 0; + background-color: white; + z-index: 20; + } + + .user_preview .card { + width: 300px; + + .preview_avatar_image { + max-width: 50%; + + img { + max-width: 100%; + } + } + } + } + +} /* high contrast */ @import "scss/high_contrast"; diff --git a/app/templates/base.html b/app/templates/base.html index d48d00e8..4070cce7 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -7,7 +7,7 @@ [deleted] {% endif -%} {% else -%} - + {% if user.avatar_id and not low_bandwidth and not collapsed -%} {% endif -%} @@ -33,6 +33,12 @@ [{{ user_note | truncate(12, True) }}] {% endif -%} {% endif -%} +
{% endif -%} {% endmacro -%} diff --git a/app/templates/chat/chat_options.html b/app/templates/chat/chat_options.html index 211673c1..ab773be7 100644 --- a/app/templates/chat/chat_options.html +++ b/app/templates/chat/chat_options.html @@ -1,36 +1,15 @@ -{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %} - {% extends 'themes/' + theme() + '/base.html' %} -{% else %} - {% extends "base.html" %} -{% endif %} -{% set active_child = 'chats' %} -{% from 'bootstrap/form.html' import render_form %} - -{% block app_content %} -
- -
-{% endblock %} \ No newline at end of file +
  • + {{ _('Delete conversation') }}
  • +{% for member in conversation.members %} + {% if member.id != current_user.id %} +
  • + {{ _('Block @%(author_name)s', author_name=member.display_name()) }}
  • + {% endif %} +{% endfor %} +{% for instance in conversation.instances() %} +
  • + {{ _("Block chats and posts from instance: %(name)s", name=instance.domain) }}
  • +{% endfor %} +
  • + {{ _('Report to moderators') }}
  • +

    {{ _('If you are reporting abuse then do not delete the conversation - moderators will not be able to read it if you delete it.') }}

    diff --git a/app/templates/chat/conversation.html b/app/templates/chat/conversation.html index 62aa8e09..f2b8cef4 100644 --- a/app/templates/chat/conversation.html +++ b/app/templates/chat/conversation.html @@ -72,7 +72,23 @@ {% endfor %} {{ render_form(form) }} - {{ _('Options') }} + {% endif %} @@ -80,4 +96,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/post/_post_full.html b/app/templates/post/_post_full.html index 9ae42048..d1725134 100644 --- a/app/templates/post/_post_full.html +++ b/app/templates/post/_post_full.html @@ -16,9 +16,9 @@ {% endif -%}

    {% if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator(current_user) -%} - {% endif -%}submitted {{ arrow.get(post.posted_at).humanize(locale=locale) }} by + {% endif -%}submitted by {{ render_username(post.author) }} - {% if post.edited_at -%} edited {{ arrow.get(post.edited_at).humanize(locale=locale) }}{% endif -%} + {% if post.edited_at -%} edited

    {% if post.type == POST_TYPE_IMAGE -%}
    @@ -181,7 +181,19 @@ {% endif -%}
    - +
    diff --git a/app/templates/post/_post_reply_teaser.html b/app/templates/post/_post_reply_teaser.html index 1f57ea73..351dc05f 100644 --- a/app/templates/post/_post_reply_teaser.html +++ b/app/templates/post/_post_reply_teaser.html @@ -34,7 +34,7 @@ {% endif -%}
    - {{ arrow.get(post_reply.posted_at).humanize(locale=locale) }}{% if post_reply.edited_at -%}, edited {{ arrow.get(post_reply.edited_at).humanize(locale=locale) }}{% endif -%} + {% if post_reply.edited_at -%}, edited {% endif -%}
    {% if post_reply.reports and current_user.is_authenticated and post_reply.post.community.is_moderator(current_user) -%} @@ -95,7 +95,22 @@
    diff --git a/app/templates/post/_post_teaser.html b/app/templates/post/_post_teaser.html index 91541cf2..52f6b042 100644 --- a/app/templates/post/_post_teaser.html +++ b/app/templates/post/_post_teaser.html @@ -9,7 +9,7 @@ {# do nothing - blocked by keyword filter #} {% else -%} {% endif -%}
    - -
    + + diff --git a/app/templates/user/edit_profile.html b/app/templates/user/edit_profile.html index be9af653..f876a24d 100644 --- a/app/templates/user/edit_profile.html +++ b/app/templates/user/edit_profile.html @@ -44,6 +44,28 @@ {% endif %} {% endif %} +
    + {{ _('Extra fields') }} +

    {{ _('Your homepage, pronouns, age, etc.') }}

    + + + + + + + + + + + + + + + + + +
    {{ render_field(form.extra_label_1) }}{{ render_field(form.extra_text_1) }}
    {{ render_field(form.extra_label_2) }}{{ render_field(form.extra_text_2) }}
    {{ render_field(form.extra_label_3) }}{{ render_field(form.extra_text_3) }}
    {{ render_field(form.extra_label_4) }}{{ render_field(form.extra_text_4) }}
    +
    {{ render_field(form.bot) }} {{ render_field(form.matrixuserid) }} e.g. @something:matrix.org. Include leading @ and use : before server @@ -69,7 +91,8 @@ hx-swap="outerHTML">{{ _('Remove image') }}

    {% endif %} - {{ render_field(form.submit) }} + +

    {{ render_field(form.submit) }}

    {{ _('Delete account') }} diff --git a/app/templates/user/show_profile.html b/app/templates/user/show_profile.html index 1afbddd2..a3aad723 100644 --- a/app/templates/user/show_profile.html +++ b/app/templates/user/show_profile.html @@ -119,11 +119,26 @@ {% 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 %} + {% if current_user.is_authenticated %}{{ _('Note') }}: {{ user.get_note(current_user) }}
    {% endif %}

    {{ user.about_html|safe }}
    + {% if user.extra_fields -%} +
      + {% for field in user.extra_fields -%} +
    • +

      {{ field.label }}
      + {% if field.text.startswith('http') -%} + {{ field.text }} + {% else -%} + {{ field.text }} + {% endif -%} +

      +
    • + {% endfor -%} +
    + {% endif -%} {% if posts %}

    Posts

    diff --git a/app/templates/user/user_preview.html b/app/templates/user/user_preview.html new file mode 100644 index 00000000..fa98f797 --- /dev/null +++ b/app/templates/user/user_preview.html @@ -0,0 +1,57 @@ +
    +
    +
    + {% if user.avatar_id -%} +
    + +
    + {% endif -%} + +
    +
    +
    +

    {{ _('Instance') }}: {{ user.instance_domain() }} + {% 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') }}
    + {% endif %} + {{ _('Attitude') }}: {{ (user.attitude * 100) | round | int }}%
    + {% 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 %}{{ _('Note') }}: {{ user.get_note(current_user) }}
    {% endif %} +

    +
    + {{ user.about_html|safe }} +
    + {% if user.extra_fields -%} +
      + {% for field in user.extra_fields -%} +
    • +

      {{ field.label }}
      + {% if field.text.startswith('http') -%} + {{ field.text }} + {% else -%} + {{ field.text }} + {% endif -%} +

      +
    • + {% endfor -%} +
    + {% endif -%} +
    +
    + +
    +
    \ No newline at end of file diff --git a/app/user/forms.py b/app/user/forms.py index e17407ce..52163f0d 100644 --- a/app/user/forms.py +++ b/app/user/forms.py @@ -14,6 +14,14 @@ class ProfileForm(FlaskForm): password_field = PasswordField(_l('Set new password'), validators=[Optional(), Length(min=1, max=50)], render_kw={"autocomplete": 'new-password'}) about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5}) + extra_label_1 = StringField(_l('Extra field 1 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')}) + extra_text_1 = StringField(_l('Extra field 1 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')}) + extra_label_2 = StringField(_l('Extra field 2 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')}) + extra_text_2 = StringField(_l('Extra field 2 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')}) + extra_label_3 = StringField(_l('Extra field 3 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')}) + extra_text_3 = StringField(_l('Extra field 3 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')}) + extra_label_4 = StringField(_l('Extra field 4 - label'), validators=[Optional(), Length(max=50)], render_kw={"placeholder": _l('Label')}) + extra_text_4 = StringField(_l('Extra field 4 - text'), validators=[Optional(), Length(max=256)], render_kw={"placeholder": _l('Content')}) matrixuserid = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)], render_kw={'autocomplete': 'off'}) profile_file = FileField(_l('Avatar image'), render_kw={'accept': 'image/*'}) diff --git a/app/user/routes.py b/app/user/routes.py index fbde47f4..bae7f1c4 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -16,7 +16,8 @@ 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, UserNote + InstanceBlock, NotificationSubscription, PostBookmark, PostReplyBookmark, read_posts, Topic, UserNote, \ + UserExtraField from app.user import bp from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm, \ FilterForm, KeywordFilterEditForm, RemoteFollowForm, ImportExportForm, UserNoteForm @@ -129,6 +130,15 @@ def edit_profile(actor): current_user.about = piefed_markdown_to_lemmy_markdown(form.about.data) current_user.about_html = markdown_to_html(form.about.data) current_user.matrix_user_id = form.matrixuserid.data + current_user.extra_fields = [] + if form.extra_label_1.data.strip() != '' and form.extra_text_1.data.strip() != '': + current_user.extra_fields.append(UserExtraField(label=form.extra_label_1.data.strip(), text=form.extra_text_1.data.strip())) + if form.extra_label_2.data.strip() != '' and form.extra_text_2.data.strip() != '': + current_user.extra_fields.append(UserExtraField(label=form.extra_label_2.data.strip(), text=form.extra_text_2.data.strip())) + if form.extra_label_3.data.strip() != '' and form.extra_text_3.data.strip() != '': + current_user.extra_fields.append(UserExtraField(label=form.extra_label_3.data.strip(), text=form.extra_text_3.data.strip())) + if form.extra_label_4.data.strip() != '' and form.extra_text_4.data.strip() != '': + current_user.extra_fields.append(UserExtraField(label=form.extra_label_4.data.strip(), text=form.extra_text_4.data.strip())) current_user.bot = form.bot.data profile_file = request.files['profile_file'] if profile_file and profile_file.filename != '': @@ -169,7 +179,13 @@ def edit_profile(actor): form.title.data = current_user.title form.email.data = current_user.email form.about.data = current_user.about + i = 1 + for extra_field in current_user.extra_fields: + getattr(form, f"extra_label_{i}").data = extra_field.label + getattr(form, f"extra_text_{i}").data = extra_field.text + i += 1 form.matrixuserid.data = current_user.matrix_user_id + form.bot.data = current_user.bot form.password_field.data = '' return render_template('user/edit_profile.html', title=_('Edit profile'), form=form, user=current_user, @@ -1363,3 +1379,11 @@ def edit_user_note(actor): return render_template('user/edit_note.html', title=_('Edit note'), form=form, user=user, menu_topics=menu_topics(), site=g.site) + + +@bp.route('/user//preview') +def user_preview(user_id): + user = User.query.get_or_404(user_id) + if (user.deleted or user.banned) and current_user.is_anonymous: + abort(404) + return render_template('user/user_preview.html', user=user) diff --git a/app/utils.py b/app/utils.py index e84858ae..f11652be 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1257,3 +1257,23 @@ def community_ids_from_instances(instance_ids) -> List[int]: def get_task_session() -> Session: # Use the same engine as the main app, but create an independent session return Session(bind=db.engine) + + +user2_cache = {} + + +def jaccard_similarity(user1_upvoted: set, user2_id: int): + if user2_id not in user2_cache: + user2_upvoted_posts = ['post/' + str(id) for id in recently_upvoted_posts(user2_id)] + user2_upvoted_replies = ['reply/' + str(id) for id in recently_upvoted_post_replies(user2_id)] + user2_cache[user2_id] = set(user2_upvoted_posts + user2_upvoted_replies) + + user2_upvoted = user2_cache[user2_id] + + if len(user2_upvoted) > 12: + intersection = len(user1_upvoted.intersection(user2_upvoted)) + union = len(user1_upvoted.union(user2_upvoted)) + + return (intersection / union) * 100 + else: + return 0 diff --git a/migrations/versions/f961f446ae17_user_extra_fields.py b/migrations/versions/f961f446ae17_user_extra_fields.py new file mode 100644 index 00000000..40f22f88 --- /dev/null +++ b/migrations/versions/f961f446ae17_user_extra_fields.py @@ -0,0 +1,41 @@ +"""user extra fields + +Revision ID: f961f446ae17 +Revises: 1189f921aca6 +Create Date: 2024-12-22 14:56:43.714502 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f961f446ae17' +down_revision = '1189f921aca6' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_extra_field', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('label', sa.String(length=50), nullable=True), + sa.Column('text', sa.String(length=256), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user_extra_field', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_user_extra_field_user_id'), ['user_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user_extra_field', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_extra_field_user_id')) + + op.drop_table('user_extra_field') + # ### end Alembic commands ###