import community subscriptions from lemmy

This commit is contained in:
rimu 2024-01-07 09:29:36 +13:00
parent 47cdf79b20
commit cd4fa6ad25
8 changed files with 119 additions and 17 deletions

View file

@ -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, private_mods=activity_json['privateMods'] if 'privateMods' in activity_json else False,
created_at=activity_json['published'] if 'published' in activity_json else utcnow(), created_at=activity_json['published'] if 'published' in activity_json else utcnow(),
last_active=activity_json['updated'] if 'updated' 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_public_url=activity_json['id'],
ap_profile_id=activity_json['id'], ap_profile_id=activity_json['id'],
ap_followers_url=activity_json['followers'], ap_followers_url=activity_json['followers'],

View file

@ -291,6 +291,7 @@ def unsubscribe_everyone_then_delete_task(community_id):
sleep(5) sleep(5)
community.delete_dependencies() 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.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']) @bp.route('/topics', methods=['GET'])

View file

@ -230,11 +230,13 @@ def subscribe(actor):
banned = CommunityBan.query.filter_by(user_id=current_user.id, community_id=community.id).first() banned = CommunityBan.query.filter_by(user_id=current_user.id, community_id=community.id).first()
if banned: if banned:
flash('You cannot join this community') flash('You cannot join this community')
else:
member = CommunityMember(user_id=current_user.id, community_id=community.id) member = CommunityMember(user_id=current_user.id, community_id=community.id)
db.session.add(member) db.session.add(member)
db.session.commit() db.session.commit()
flash('You joined ' + community.title) flash('You joined ' + community.title)
referrer = request.headers.get('Referer', None) referrer = request.headers.get('Referer', None)
cache.delete_memoized(community_membership, current_user, community)
if referrer is not None: if referrer is not None:
return redirect(referrer) return redirect(referrer)
else: else:

View file

@ -53,7 +53,7 @@ def search_for_community(address: str):
community_json = community_data.json() community_json = community_data.json()
community_data.close() community_data.close()
if community_json['type'] == 'Group': 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: if current_app.debug:
retrieve_mods_and_backfill(community.id) retrieve_mods_and_backfill(community.id)
else: else:

View file

@ -21,7 +21,7 @@
<div class="card-title">{{ _('Found a community:') }}</div> <div class="card-title">{{ _('Found a community:') }}</div>
<div class="card-body"> <div class="card-body">
<p> <p>
<a href="/c/{{ new_community.link() }}"><img src="{{ new_community.icon_image()}}" class="community_icon rounded-circle" /></a> <a href="/c/{{ new_community.link() }}"><img src="{{ new_community.icon_image()}}" class="community_icon rounded-circle" style="width: 30px; vertical-align: middle;" /></a>
<a href="/c/{{ new_community.link() }}">{{ new_community.title }}@{{ new_community.ap_domain }}</a> <a href="/c/{{ new_community.link() }}">{{ new_community.title }}@{{ new_community.ap_domain }}</a>
</p> </p>
<p> {% if subscribed %} <p> {% if subscribed %}

View file

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %} {% from 'bootstrap/form.html' import render_field %}
{% block app_content %} {% block app_content %}
<div class="row"> <div class="row">
@ -12,8 +12,16 @@
</ol> </ol>
</nav> </nav>
<h1 class="mt-2">{{ _('Change settings') }}</h1> <h1 class="mt-2">{{ _('Change settings') }}</h1>
<form method='post'> <form method='post' enctype="multipart/form-data">
{{ 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) }}
</form> </form>
</div> </div>
</div> </div>

View file

@ -33,6 +33,7 @@ class SettingsForm(FlaskForm):
searchable = BooleanField(_l('Show profile in user list')) searchable = BooleanField(_l('Show profile in user list'))
indexable = BooleanField(_l('Allow search engines to index this profile')) indexable = BooleanField(_l('Allow search engines to index this profile'))
manually_approves_followers = BooleanField(_l('Manually approve followers')) manually_approves_followers = BooleanField(_l('Manually approve followers'))
import_file = FileField(_('Import community subscriptions and user blocks from Lemmy'))
submit = SubmitField(_l('Save settings')) submit = SubmitField(_l('Save settings'))

View file

@ -1,21 +1,23 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep 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_login import login_user, logout_user, current_user, login_required
from flask_babel import _ from flask_babel import _
from app import db, cache, celery from app import db, cache, celery
from app.activitypub.signature import post_request from app.activitypub.signature import post_request
from app.activitypub.util import default_context from app.activitypub.util import default_context, find_actor_or_create
from app.community.util import save_icon_file, save_banner_file 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, \ 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 import bp
from app.user.forms import ProfileForm, SettingsForm, DeleteAccountForm, ReportUserForm 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, \ 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 from sqlalchemy import desc, or_, text
import os
@bp.route('/people', methods=['GET', 'POST']) @bp.route('/people', methods=['GET', 'POST'])
@ -142,7 +144,24 @@ def change_settings(actor):
current_user.show_nsfl = form.nsfl.data current_user.show_nsfl = form.nsfl.data
current_user.searchable = form.searchable.data current_user.searchable = form.searchable.data
current_user.indexable = form.indexable.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() db.session.commit()
flash(_('Your changes have been saved.'), 'success') flash(_('Your changes have been saved.'), 'success')
@ -154,7 +173,6 @@ def change_settings(actor):
form.nsfl.data = current_user.show_nsfl form.nsfl.data = current_user.show_nsfl
form.searchable.data = current_user.searchable form.searchable.data = current_user.searchable
form.indexable.data = current_user.indexable 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) 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() db.session.commit()
flash(_('All notifications marked as read.')) flash(_('All notifications marked as read.'))
return redirect(url_for('user.notifications')) 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()