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 @@ {% 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..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 ###