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 %}
-
-
-
-
-
{{ _('Options for conversation with "%(member_names)s"', member_names=conversation.member_names(current_user.id)) }}
-
-
{{ _('If you are reporting abuse then do not delete the conversation - moderators will not be able to read it if you delete it.') }}
-
-
-
-
-{% 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 {{ arrow.get(post.posted_at).humanize(locale=locale) }} by
{{ render_username(post.author) }}
- {% if post.edited_at -%} edited {{ arrow.get(post.edited_at).humanize(locale=locale) }}{% endif -%}
+ {% if post.edited_at -%} edited {{ arrow.get(post.edited_at).humanize(locale=locale) }}{% endif -%}
{% 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 -%}
+ {{ 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.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 -%}
+ {% if content_blocked -%} title="{{ _('Filtered: ') }}{{ content_blocked }}"{% else %} title="Post: {{ post.title }}" aria-label="Post: {{ post.title }}"{% endif %} tabindex="0">
{% if post.type == POST_TYPE_ARTICLE %}
{% include "post/post_teaser/_article.html" -%}
diff --git a/app/templates/post/_post_voting_buttons.html b/app/templates/post/_post_voting_buttons.html
index 50436dbb..a4fc2d40 100644
--- a/app/templates/post/_post_voting_buttons.html
+++ b/app/templates/post/_post_voting_buttons.html
@@ -7,19 +7,23 @@
{% endif -%}
{{ shorten_number(post.up_votes - post.down_votes) }}
- {% if can_downvote(current_user, post.community) and not disable_voting -%}
+ {%- if can_downvote(current_user, post.community) and not disable_voting -%}
- {% endif -%}
+ {%- endif -%}
{% else -%}
-
-
-
-
{{ shorten_number(post.up_votes - post.down_votes) }}
-
-
-
+ {% if not disable_voting -%}
+
+
+
+ {% endif -%}
+
{{ shorten_number(post.up_votes - post.down_votes) }}
+ {%- if not disable_voting -%}
+
+
+
+ {%- endif -%}
{% endif -%}
diff --git a/app/templates/post/post_options.html b/app/templates/post/post_options.html
index 5f358e64..1f15110d 100644
--- a/app/templates/post/post_options.html
+++ b/app/templates/post/post_options.html
@@ -1,83 +1,62 @@
-{% 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 -%}
-
-
-
-
-
{{ _('Options for "%(post_title)s"', post_title=post.title) }} {% if current_user.is_authenticated -%}{% include 'post/_post_notification_toggle.html' -%}{% endif -%}
-
-
{{ _('If you want to perform more than one of these (e.g. block and report), hold down Ctrl and click, then complete the operation in the new tabs that open.') }}
-
-
-
-
-{% endblock -%}
+{% if current_user.is_authenticated -%}
+ {% if post.user_id == current_user.id -%}
+
+ {{ _('Edit') }}
+ {% endif -%}
+ {% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
+ {% if post.deleted -%}
+
+ {{ _('Restore') }}
+
+ {{ _('Purge') }}
+ {% else -%}
+
+ {{ _('Delete') }}
+ {% endif -%}
+ {% endif -%}
+ {% if existing_bookmark -%}
+
+ {{ _('Remove bookmark') }}
+ {% else -%}
+
+ {{ _('Bookmark') }}
+ {% endif -%}
+ {% if post.user_id == current_user.id and not post.mea_culpa -%}
+
+ {{ _("I made a mistake with this post and have changed my mind about the topic") }}
+ {% endif -%}
+ {% if post.user_id != current_user.id -%}
+ {% if post.type == POST_TYPE_LINK and post.author.bot and (post.cross_posts is none or len(post.cross_posts) == 0) -%}
+
+ {{ _('Cross-post to another community') }}
+ {% endif -%}
+
+ {{ _('Block post author @%(author_name)s', author_name=post.author.user_name) }}
+
+ {{ _('Block community %(community_name)s', community_name=post.community.display_name()) }}
+ {% if post.community.is_moderator() or current_user.is_admin() -%}
+
+ {{ _('Ban post author @%(author_name)s from %(community_name)s', author_name=post.author.user_name, community_name=post.community.title) }}
+ {% endif -%}
+ {% if post.domain_id -%}
+
+ {{ _('Block domain %(domain)s', domain=post.domain.name) }}
+ {% endif -%}
+ {% if post.instance_id and post.instance_id != 1 -%}
+
+ {{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }}
+ {% endif -%}
+ {% endif -%}
+{% endif -%}
+{% if post.ap_id -%}
+
+ {{ _('View original on %(domain)s', domain=post.instance.domain) }}
+{% endif -%}
+
+ {{ _('Report to moderators') }}
+{% if current_user.is_authenticated and (current_user.is_admin() or current_user.is_staff()) -%}
+
+ {{ _('View Voting Activity') }}
+
+ {{ _('Fixup from remote') }}
+{% endif -%}
diff --git a/app/templates/post/post_reply_options.html b/app/templates/post/post_reply_options.html
index 339deb57..a8061a2b 100644
--- a/app/templates/post/post_reply_options.html
+++ b/app/templates/post/post_reply_options.html
@@ -1,63 +1,42 @@
-{% 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 -%}
-
-
-
-
-
{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}
-
-
{{ _('If you want to perform more than one of these (e.g. block and report), hold down Ctrl and click, then complete the operation in the new tabs that open.') }}
-
-
-
-
-{% endblock -%}
+{% if current_user.is_authenticated -%}
+ {% if post_reply.user_id == current_user.id -%}
+
+ {{ _('Edit') }}
+ {% endif -%}
+ {% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() -%}
+ {% if post_reply.deleted -%}
+
+ {{ _('Restore') }}
+ {% if not post_reply.has_replies() -%}
+ {% if post.community.is_moderator() or current_user.is_admin() or (post_reply.user_id == current_user.id and post_reply.deleted_by == post_reply.user_id) -%}
+
+ {{ _('Purge') }}
+ {% endif -%}
+ {% endif -%}
+ {% else -%}
+
+ {{ _('Delete') }}
+ {% endif -%}
+ {% endif -%}
+ {% if existing_bookmark -%}
+
+ {{ _('Remove bookmark') }}
+ {% else -%}
+
+ {{ _('Bookmark') }}
+ {% endif -%}
+ {% if post_reply.user_id != current_user.id -%}
+
+ {{ _('Block author @%(author_name)s', author_name=post_reply.author.user_name) }}
+ {% if post_reply.instance_id and post_reply.instance_id != 1 -%}
+
+ {{ _("Hide every post from author's instance: %(name)s", name=post_reply.instance.domain) }}
+ {% endif -%}
+ {% endif -%}
+ {% if current_user.is_authenticated and (current_user.is_admin() or current_user.is_staff()) -%}
+
+ {{ _('View Voting Activity') }}
+ {% endif -%}
+{% endif -%}
+
+ {{ _('Report to moderators') }}
diff --git a/app/templates/post/post_teaser/_title.html b/app/templates/post/post_teaser/_title.html
index 41606ba4..4860b41d 100644
--- a/app/templates/post/post_teaser/_title.html
+++ b/app/templates/post/post_teaser/_title.html
@@ -26,4 +26,4 @@
{% if show_post_community -%}
{% if post.community.icon_id and not low_bandwidth %}{% endif -%}
c/{{ post.community.name }} {% endif -%}
- by {{ render_username(post.author) }} {{ post.posted_at_localized(sort, locale) }}
\ No newline at end of file
+ by {{ render_username(post.author) }}
{{ post.posted_at_localized(sort, locale) }}
diff --git a/app/templates/post/post_teaser/_utilities_bar.html b/app/templates/post/post_teaser/_utilities_bar.html
index dec5cab3..51909f0d 100644
--- a/app/templates/post/post_teaser/_utilities_bar.html
+++ b/app/templates/post/post_teaser/_utilities_bar.html
@@ -20,6 +20,18 @@
{% 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 @@
{{ _('Enable markdown editor') }}
{% 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 -%}
+
+ {% 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 -%}
+
+ {% 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 ###
+ +
+