diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 5ac0aae9..81c61be3 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -391,7 +391,7 @@ def actor_json_to_model(activity_json, address, server): private_mods=activity_json['privateMods'] if 'privateMods' in activity_json else False, created_at=activity_json['published'] if 'published' in activity_json else utcnow(), last_active=activity_json['updated'] if 'updated' in activity_json else utcnow(), - ap_id=f"{address[1:]}", + ap_id=f"{address[1:]}@{server}" if address.startswith('!') else f"{address}@{server}", ap_public_url=activity_json['id'], ap_profile_id=activity_json['id'], ap_followers_url=activity_json['followers'], diff --git a/app/admin/routes.py b/app/admin/routes.py index bf10ce37..29c10b39 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -291,6 +291,7 @@ def unsubscribe_everyone_then_delete_task(community_id): sleep(5) community.delete_dependencies() db.session.delete(community) # todo: when a remote community is deleted it will be able to be re-created by using the 'Add remote' function. Not ideal. Consider soft-delete. + db.session.commit() @bp.route('/topics', methods=['GET']) diff --git a/app/community/routes.py b/app/community/routes.py index e0bd0b4b..08a37ad4 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -230,11 +230,13 @@ def subscribe(actor): banned = CommunityBan.query.filter_by(user_id=current_user.id, community_id=community.id).first() if banned: flash('You cannot join this community') - member = CommunityMember(user_id=current_user.id, community_id=community.id) - db.session.add(member) - db.session.commit() - flash('You joined ' + community.title) + else: + member = CommunityMember(user_id=current_user.id, community_id=community.id) + db.session.add(member) + db.session.commit() + flash('You joined ' + community.title) referrer = request.headers.get('Referer', None) + cache.delete_memoized(community_membership, current_user, community) if referrer is not None: return redirect(referrer) else: diff --git a/app/community/util.py b/app/community/util.py index 7cf2be9c..5905f31e 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -53,7 +53,7 @@ def search_for_community(address: str): community_json = community_data.json() community_data.close() if community_json['type'] == 'Group': - community = actor_json_to_model(community_json, address, server) + community = actor_json_to_model(community_json, name, server) if current_app.debug: retrieve_mods_and_backfill(community.id) else: diff --git a/app/templates/community/add_remote.html b/app/templates/community/add_remote.html index a7499162..443bafc4 100644 --- a/app/templates/community/add_remote.html +++ b/app/templates/community/add_remote.html @@ -21,7 +21,7 @@
{{ _('Found a community:') }}

- + {{ new_community.title }}@{{ new_community.ap_domain }}

{% if subscribed %} diff --git a/app/templates/user/edit_settings.html b/app/templates/user/edit_settings.html index 9c0f04f2..9773ab22 100644 --- a/app/templates/user/edit_settings.html +++ b/app/templates/user/edit_settings.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% from 'bootstrap/form.html' import render_form %} +{% from 'bootstrap/form.html' import render_field %} {% block app_content %}

@@ -12,8 +12,16 @@

{{ _('Change settings') }}

-
- {{ render_form(form) }} + + {{ form.csrf_token() }} + {{ render_field(form.newsletter) }} + {{ render_field(form.ignore_bots) }} + {{ render_field(form.nsfw) }} + {{ render_field(form.nsfl) }} + {{ render_field(form.searchable) }} + {{ render_field(form.indexable) }} + {{ render_field(form.import_file) }} + {{ render_field(form.submit) }}
diff --git a/app/user/forms.py b/app/user/forms.py index 6d5b4880..1ea5e73c 100644 --- a/app/user/forms.py +++ b/app/user/forms.py @@ -33,6 +33,7 @@ class SettingsForm(FlaskForm): searchable = BooleanField(_l('Show profile in user list')) indexable = BooleanField(_l('Allow search engines to index this profile')) manually_approves_followers = BooleanField(_l('Manually approve followers')) + import_file = FileField(_('Import community subscriptions and user blocks from Lemmy')) submit = SubmitField(_l('Save settings')) diff --git a/app/user/routes.py b/app/user/routes.py index bdb508c8..f64e4cb3 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -1,21 +1,23 @@ from datetime import datetime, timedelta from time import sleep -from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort +from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort, json from flask_login import login_user, logout_user, current_user, login_required from flask_babel import _ from app import db, cache, celery from app.activitypub.signature import post_request -from app.activitypub.util import default_context -from app.community.util import save_icon_file, save_banner_file +from app.activitypub.util import default_context, find_actor_or_create +from app.community.util import save_icon_file, save_banner_file, retrieve_mods_and_backfill +from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_PENDING from app.models import Post, Community, CommunityMember, User, PostReply, PostVote, Notification, utcnow, File, Site, \ - Instance, Report, UserBlock + Instance, Report, UserBlock, CommunityBan, CommunityJoinRequest, CommunityBlock from app.user import bp from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm from app.utils import get_setting, render_template, markdown_to_html, user_access, markdown_to_text, shorten_string, \ - is_image_url + is_image_url, ensure_directory_exists, gibberish, file_get_contents, community_membership from sqlalchemy import desc, or_, text +import os @bp.route('/people', methods=['GET', 'POST']) @@ -142,7 +144,24 @@ def change_settings(actor): current_user.show_nsfl = form.nsfl.data current_user.searchable = form.searchable.data current_user.indexable = form.indexable.data - current_user.ap_manually_approves_followers = form.manually_approves_followers.data + import_file = request.files['import_file'] + if import_file and import_file.filename != '': + file_ext = os.path.splitext(import_file.filename)[1] + if file_ext.lower() != '.json': + abort(400) + new_filename = gibberish(15) + '.json' + + directory = f'app/static/media/' + + # save the file + final_place = os.path.join(directory, new_filename + file_ext) + import_file.save(final_place) + + # import settings in background task + import_settings(final_place) + + flash(_('Your subscriptions and blocks are being imported. If you have many it could take a few minutes.')) + db.session.commit() flash(_('Your changes have been saved.'), 'success') @@ -154,7 +173,6 @@ def change_settings(actor): form.nsfl.data = current_user.show_nsfl form.searchable.data = current_user.searchable form.indexable.data = current_user.indexable - form.manually_approves_followers.data = current_user.ap_manually_approves_followers return render_template('user/edit_settings.html', title=_('Edit profile'), form=form, user=current_user) @@ -475,3 +493,75 @@ def notifications_all_read(): db.session.commit() flash(_('All notifications marked as read.')) return redirect(url_for('user.notifications')) + + +def import_settings(filename): + if current_app.debug: + import_settings_task(current_user.id, filename) + else: + import_settings_task.delay(current_user.id, filename) + + +@celery.task +def import_settings_task(user_id, filename): + user = User.query.get(user_id) + contents = file_get_contents(filename) + contents_json = json.loads(contents) + + # Follow communities + for community_ap_id in contents_json['followed_communities'] if 'followed_communities' in contents_json else []: + community = find_actor_or_create(community_ap_id) + if community: + if community.posts.count() == 0: + if current_app.debug: + retrieve_mods_and_backfill(community.id) + else: + retrieve_mods_and_backfill.delay(community.id) + if community_membership(user, community) != SUBSCRIPTION_MEMBER and community_membership( + user, community) != SUBSCRIPTION_PENDING: + if not community.is_local(): + # send ActivityPub message to remote community, asking to follow. Accept message will be sent to our shared inbox + join_request = CommunityJoinRequest(user_id=user.id, community_id=community.id) + db.session.add(join_request) + db.session.commit() + follow = { + "actor": f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}", + "to": [community.ap_profile_id], + "object": community.ap_profile_id, + "type": "Follow", + "id": f"https://{current_app.config['SERVER_NAME']}/activities/follow/{join_request.id}" + } + success = post_request(community.ap_inbox_url, follow, user.private_key, + user.profile_id() + '#main-key') + if not success: + sleep(5) # give them a rest + else: # for local communities, joining is instant + banned = CommunityBan.query.filter_by(user_id=user.id, community_id=community.id).first() + if not banned: + member = CommunityMember(user_id=user.id, community_id=community.id) + db.session.add(member) + db.session.commit() + cache.delete_memoized(community_membership, current_user, community) + + for community_ap_id in contents_json['blocked_communities'] if 'blocked_communities' in contents_json else []: + community = find_actor_or_create(community_ap_id) + if community: + existing_block = CommunityBlock.query.filter_by(user_id=user.id, community_id=community.id).first() + if not existing_block: + block = CommunityBlock(user_id=user.id, community_id=community.id) + db.session.add(block) + + for user_ap_id in contents_json['blocked_users'] if 'blocked_users' in contents_json else []: + blocked_user = find_actor_or_create(user_ap_id) + if blocked_user: + existing_block = UserBlock.query.filter_by(blocker_id=user.id, blocked_id=blocked_user.id).first() + if not existing_block: + user_block = UserBlock(blocker_id=user.id, blocked_id=blocked_user.id) + db.session.add(user_block) + if not blocked_user.is_local(): + ... # todo: federate block + + for instance_domain in contents_json['blocked_instances']: + ... + + db.session.commit()