diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 5cab1f1a..ea44912a 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -569,7 +569,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/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 37049fcb..ff04d57a 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -49,7 +49,8 @@ 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_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) @@ -102,9 +103,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, @@ -962,4 +963,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 774cd012..05e435ef 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']) diff --git a/app/models.py b/app/models.py index 2c999d45..f7c5df44 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']) @@ -466,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() @@ -750,6 +762,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) @@ -803,6 +819,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) diff --git a/app/post/routes.py b/app/post/routes.py index 8af417ef..79a5e4d3 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 ], @@ -173,6 +183,17 @@ 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 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 else: replies = post_replies(post.id, sort) 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()