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,
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'],

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@
<div class="card-title">{{ _('Found a community:') }}</div>
<div class="card-body">
<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>
</p>
<p> {% if subscribed %}

View file

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

View file

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

View file

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