extra fields on profiles ala Mastodon

This commit is contained in:
rimu 2024-12-22 15:38:40 +13:00
parent 91a74e9573
commit 3662749188
10 changed files with 146 additions and 11 deletions

View file

@ -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'<https://{current_app.config["SERVER_NAME"]}/u/{actor}>; rel="alternate"; type="text/html"')

View file

@ -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()

View file

@ -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)

View file

@ -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;

View file

@ -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 {

View file

@ -44,6 +44,28 @@
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="about">{{ _('Enable markdown editor') }}</a>
{% endif %}
{% endif %}
<fieldset class="coolfieldset mt-2 mb-3">
<legend>{{ _('Extra fields') }}</legend>
<p>{{ _('Your homepage, pronouns, age, etc.') }}</p>
<table class="hide-labels">
<tr>
<td>{{ render_field(form.extra_label_1) }}</td>
<td>{{ render_field(form.extra_text_1) }}</td>
</tr>
<tr>
<td>{{ render_field(form.extra_label_2) }}</td>
<td>{{ render_field(form.extra_text_2) }}</td>
</tr>
<tr>
<td>{{ render_field(form.extra_label_3) }}</td>
<td>{{ render_field(form.extra_text_3) }}</td>
</tr>
<tr>
<td>{{ render_field(form.extra_label_4) }}</td>
<td>{{ render_field(form.extra_text_4) }}</td>
</tr>
</table>
</fieldset>
{{ render_field(form.bot) }}
{{ render_field(form.matrixuserid) }}
<small class="field_hint">e.g. @something:matrix.org. Include leading @ and use : before server</small>
@ -69,7 +91,8 @@
hx-swap="outerHTML">{{ _('Remove image') }}</a></p>
<div id="cover_div" class="community_header mb-4" style="display: none; height: 240px; background-image: url({{ user.cover_image() }});"></div>
{% endif %}
{{ render_field(form.submit) }}
<p class="mt-4">{{ render_field(form.submit) }}</p>
</form>
<p class="mt-4 pt-4">
<a class="btn btn-warning" href="{{ url_for('user.delete_account') }}">{{ _('Delete account') }}</a>

View file

@ -124,6 +124,21 @@
<div class="profile_bio">
{{ user.about_html|safe }}
</div>
{% if user.extra_fields -%}
<ul class="list-group">
{% for field in user.extra_fields -%}
<li class="list-group-item">
<p class="mb-0"><strong>{{ field.label }}</strong><br>
{% if field.text.startswith('http') -%}
<a href="{{ field.text }}" rel="nofollow noindex ugc">{{ field.text }}</a>
{% else -%}
{{ field.text }}
{% endif -%}
</p>
</li>
{% endfor -%}
</ul>
{% endif -%}
{% if posts %}
<h2 class="mt-4">Posts</h2>
<div class="post_list">

View file

@ -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/*'})

View file

@ -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,

View file

@ -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 ###