From e8f7551e061a611267249d5569d25219cf4fff19 Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 00:15:10 +0000 Subject: [PATCH 1/8] Avoiding crashes from adding remotes: If remote community is missing If remote community doesn't have a 'featured' url (e.g. KBIN) If remote community returns empty/broken outbox (e.g. KBIN returns {}) with 200 OK --- app/activitypub/util.py | 2 +- app/community/routes.py | 8 ++++---- app/community/util.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 239dedcf..ad087afd 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -564,7 +564,7 @@ def actor_json_to_model(activity_json, address, server): ap_followers_url=activity_json['followers'], ap_inbox_url=activity_json['endpoints']['sharedInbox'], ap_outbox_url=activity_json['outbox'], - ap_featured_url=activity_json['featured'], + ap_featured_url=activity_json['featured'] if 'featured' in activity_json else '', ap_moderators_url=mods_url, ap_fetched_at=utcnow(), ap_domain=server, diff --git a/app/community/routes.py b/app/community/routes.py index 21f5a69a..d6af868b 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -102,9 +102,9 @@ def add_remote(): flash(_('Community not found.'), 'warning') else: flash(_('Community not found. If you are searching for a nsfw community it is blocked by this instance.'), 'warning') - - if new_community.banned: - flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning') + else: + if new_community.banned: + flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning') return render_template('community/add_remote.html', title=_('Add remote community'), form=form, new_community=new_community, @@ -960,4 +960,4 @@ def community_moderate_banned(actor): else: abort(401) else: - abort(404) \ No newline at end of file + abort(404) diff --git a/app/community/util.py b/app/community/util.py index 44642fb0..ebfbd775 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -96,7 +96,7 @@ def retrieve_mods_and_backfill(community_id: int): if outbox_request.status_code == 200: outbox_data = outbox_request.json() outbox_request.close() - if outbox_data['type'] == 'OrderedCollection' and 'orderedItems' in outbox_data: + if 'type' in outbox_data and outbox_data['type'] == 'OrderedCollection' and 'orderedItems' in outbox_data: activities_processed = 0 for activity in outbox_data['orderedItems']: user = find_actor_or_create(activity['object']['actor']) From 4ae5b3645998d03f39325acaa2db775b73e51e6b Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 01:53:18 +0000 Subject: [PATCH 2/8] Lowercase the ap_profile_id of New Local Users and New Local Communties (to match behaviour for remote users and communities) --- app/admin/routes.py | 2 +- app/community/routes.py | 2 +- app/utils.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/admin/routes.py b/app/admin/routes.py index 2ecc2c54..beba0184 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -627,7 +627,7 @@ def admin_users_add(): private_key, public_key = RsaKeys.generate_keypair() user.private_key = private_key user.public_key = public_key - user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" + user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}".lower() user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox" user.roles.append(Role.query.get(form.role.data)) diff --git a/app/community/routes.py b/app/community/routes.py index d6af868b..0e92c61a 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -49,7 +49,7 @@ def add_local(): rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key, public_key=public_key, description_html=markdown_to_html(form.description.data), rules_html=markdown_to_html(form.rules.data), local_only=form.local_only.data, - ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data, + ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data.lower(), ap_followers_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data + '/followers', ap_domain=current_app.config['SERVER_NAME'], subscriptions_count=1, instance_id=1, low_quality='memes' in form.url.data) diff --git a/app/utils.py b/app/utils.py index 5c6d2fcd..fa556b6a 100644 --- a/app/utils.py +++ b/app/utils.py @@ -670,7 +670,7 @@ def finalize_user_setup(user, application_required=False): private_key, public_key = RsaKeys.generate_keypair() user.private_key = private_key user.public_key = public_key - user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" + user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}".lower() user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox" db.session.commit() From 4dbe485608249f50cc8c132897b255fb9d24f0b1 Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 02:02:47 +0000 Subject: [PATCH 3/8] Add ap_public_url for New Local communities --- app/community/routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/community/routes.py b/app/community/routes.py index 0e92c61a..f5083950 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -50,6 +50,7 @@ def add_local(): public_key=public_key, description_html=markdown_to_html(form.description.data), rules_html=markdown_to_html(form.rules.data), local_only=form.local_only.data, ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data.lower(), + ap_public_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data, ap_followers_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data + '/followers', ap_domain=current_app.config['SERVER_NAME'], subscriptions_count=1, instance_id=1, low_quality='memes' in form.url.data) From e3b9e5f0f7fe85c49f9cecd1adc75d19dfe7f079 Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 02:12:34 +0000 Subject: [PATCH 4/8] Add public_url() function for User and Community classes --- app/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models.py b/app/models.py index 2c999d45..d22ebb88 100644 --- a/app/models.py +++ b/app/models.py @@ -440,6 +440,10 @@ class Community(db.Model): retval = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}" return retval.lower() + def public_url(self): + result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}" + return result + def is_local(self): return self.ap_id is None or self.profile_id().startswith('https://' + current_app.config['SERVER_NAME']) @@ -750,6 +754,10 @@ class User(UserMixin, db.Model): result = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}" return result + def public_url(self): + result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}" + return result + def created_recently(self): return self.created and self.created > utcnow() - timedelta(days=7) From f63472e6bf07dc8b282cbd26057889f315ea35ff Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 02:19:49 +0000 Subject: [PATCH 5/8] Add mention_tag() function to User class --- app/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models.py b/app/models.py index d22ebb88..642d1348 100644 --- a/app/models.py +++ b/app/models.py @@ -811,6 +811,12 @@ class User(UserMixin, db.Model): reply.body = reply.body_html = '' db.session.commit() + def mention_tag(self): + if self.ap_domain is None: + return '@' + self.user_name + '@' + current_app.config['SERVER_NAME'] + else: + return '@' + self.user_name + '@' + self.ap_domain + class ActivityLog(db.Model): id = db.Column(db.Integer, primary_key=True) From caedb3e84fd6c53ebe30c11d0b423a300c40f27b Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 03:20:45 +0000 Subject: [PATCH 6/8] Copy Lemmy for top-level post reply JSON --- app/post/routes.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/post/routes.py b/app/post/routes.py index 8af417ef..1c03ca63 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -118,12 +118,12 @@ def show_post(post_id: int): reply_json = { 'type': 'Note', 'id': reply.profile_id(), - 'attributedTo': current_user.profile_id(), + 'attributedTo': current_user.public_url(), 'to': [ 'https://www.w3.org/ns/activitystreams#Public' ], 'cc': [ - community.profile_id(), + community.public_url(), post.author.public_url() ], 'content': reply.body_html, 'inReplyTo': post.profile_id(), @@ -134,20 +134,30 @@ def show_post(post_id: int): }, 'published': ap_datetime(utcnow()), 'distinguished': False, - 'audience': community.profile_id() + 'audience': community.public_url(), + 'tag': [{ + 'href': post.author.public_url(), + 'name': post.author.mention_tag(), + 'type': 'Mention' + }] } create_json = { 'type': 'Create', - 'actor': current_user.profile_id(), - 'audience': community.profile_id(), + 'actor': current_user.public_url(), + 'audience': community.public_url(), 'to': [ 'https://www.w3.org/ns/activitystreams#Public' ], 'cc': [ - community.ap_profile_id + community.public_url(), post.author.public_url() ], 'object': reply_json, - 'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}" + 'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}", + 'tag': [{ + 'href': post.author.public_url(), + 'name': post.author.mention_tag(), + 'type': 'Mention' + }] } if not community.is_local(): # this is a remote community, send it to the instance that hosts it success = post_request(community.ap_inbox_url, create_json, current_user.private_key, @@ -161,7 +171,7 @@ def show_post(post_id: int): "to": [ "https://www.w3.org/ns/activitystreams#Public" ], - "actor": community.ap_profile_id, + "actor": community.public_url(), "cc": [ community.ap_followers_url ], From 403a04df7cb219cce5c00dd693b49aa6ea9fb34b Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 04:13:51 +0000 Subject: [PATCH 7/8] Also send top-level post reply directly to post author --- app/post/routes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/post/routes.py b/app/post/routes.py index 1c03ca63..8ee73b2a 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -183,6 +183,12 @@ def show_post(post_id: int): if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): send_to_remote_instance(instance.id, community.id, announce) + # send copy of Note to post author (who won't otherwise get it if they're not subscribed to the community) + if not post.author.is_local(): + if post.author.ap_domain != community.ap_domain: + post_request(post.author.ap_inbox_url, create_json, current_user.private_key, + current_user.ap_profile_id + '#main-key') + return redirect(url_for('activitypub.post_ap', post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form else: replies = post_replies(post.id, sort) From a0e974df11d288839a63040bb3ae7f6895fb98be Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 16:38:20 +0000 Subject: [PATCH 8/8] Only send separate Note if community is remote or local community has no followers from post.author's instance --- app/models.py | 8 ++++++++ app/post/routes.py | 13 +++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/models.py b/app/models.py index 642d1348..f7c5df44 100644 --- a/app/models.py +++ b/app/models.py @@ -470,6 +470,14 @@ class Community(db.Model): instances = instances.filter(Instance.id != 1, Instance.gone_forever == False) return instances.all() + def has_followers_from_domain(self, domain: str) -> bool: + instances = Instance.query.join(User, User.instance_id == Instance.id).join(CommunityMember, CommunityMember.user_id == User.id) + instances = instances.filter(CommunityMember.community_id == self.id, CommunityMember.is_banned == False) + for instance in instances: + if instance.domain == domain: + return True + return False + def delete_dependencies(self): for post in self.posts: post.delete_dependencies() diff --git a/app/post/routes.py b/app/post/routes.py index 8ee73b2a..79a5e4d3 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -183,10 +183,15 @@ def show_post(post_id: int): if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): send_to_remote_instance(instance.id, community.id, announce) - # send copy of Note to post author (who won't otherwise get it if they're not subscribed to the community) - if not post.author.is_local(): - if post.author.ap_domain != community.ap_domain: - post_request(post.author.ap_inbox_url, create_json, current_user.private_key, + # send copy of Note to post author (who won't otherwise get it if no-one else on their instance is subscribed to the community) + if not post.author.is_local() and post.author.ap_domain != community.ap_domain: + if not community.is_local() or (community.is_local and not community.has_followers_from_domain(post.author.ap_domain)): + success = post_request(post.author.ap_inbox_url, create_json, current_user.private_key, + current_user.ap_profile_id + '#main-key') + if not success: + # sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers + personal_inbox = post.author.public_url() + '/inbox' + post_request(personal_inbox, create_json, current_user.private_key, current_user.ap_profile_id + '#main-key') return redirect(url_for('activitypub.post_ap', post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form