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/models.py b/app/models.py
index 7aae3641..457d75b7 100644
--- a/app/models.py
+++ b/app/models.py
@@ -727,6 +727,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")
@@ -2053,6 +2054,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/static/styles.css b/app/static/styles.css
index 505fec8e..fba2a5a4 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -1397,6 +1397,10 @@ time {
border-top: solid 1px #ddd;
}
+.hide-labels label {
+ display: none;
+}
+
#add_local_community_form #url {
width: 297px;
display: inline-block;
@@ -1487,10 +1491,6 @@ fieldset legend {
overflow-x: auto;
}
-.list-group-item:first-child {
- padding-top: 0;
-}
-
.skip-link:focus {
top: 0;
}
@@ -1708,6 +1708,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;
diff --git a/app/static/styles.scss b/app/static/styles.scss
index aa93518c..e27be3c6 100644
--- a/app/static/styles.scss
+++ b/app/static/styles.scss
@@ -1064,6 +1064,10 @@ time {
}
}
+.hide-labels label {
+ display: none;
+}
+
#add_local_community_form {
#url {
width: 297px;
@@ -1157,10 +1161,6 @@ fieldset {
}
}
-.list-group-item:first-child {
- padding-top: 0;
-}
-
.skip-link:focus {
top: 0;
}
@@ -1397,6 +1397,10 @@ h1 .warning_badge {
img {
max-width: 100%;
}
+
+ .list-group-item:first-child {
+ padding-top: 0;
+ }
}
[data-bs-theme=dark] .main_pane {
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 %}
+
{{ 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..5c536d8f 100644
--- a/app/templates/user/show_profile.html
+++ b/app/templates/user/show_profile.html
@@ -124,6 +124,21 @@
{{ user.about_html|safe }}
+ {% if user.extra_fields -%}
+
+ {% endif -%}
{% if posts %}
Posts
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..fe7235fc 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,
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 ###