From 6d2b722f0e572bc9ef47c204b61a7acba9267387 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 10:17:23 +1300 Subject: [PATCH 01/21] move urllib to top this needs to already be present before flask is installed, so it is the correct version when boto3 is installed. phew --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e547eee5..cd6d90a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +urllib3==1.26.1 Flask==2.3.2 python-dotenv==1.0.0 flask-wtf==1.1.1 @@ -30,4 +31,3 @@ redis==5.0.1 Werkzeug==2.3.3 pytesseract==0.3.10 sentry-sdk==1.40.6 -urllib3==1.26.1 From ad62df7c10c8aadd8e96d608ffb326129d5e6f0c Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 12:00:16 +1300 Subject: [PATCH 02/21] improve consistency of image post handling when a link post is created and the link is to an image, create an image post instead --- app/community/routes.py | 4 +++- app/community/util.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/community/routes.py b/app/community/routes.py index 21f5a69a..37049fcb 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -9,7 +9,7 @@ from sqlalchemy import or_, desc from app import db, constants, cache from app.activitypub.signature import RsaKeys, post_request -from app.activitypub.util import default_context, notify_about_post, find_actor_or_create +from app.activitypub.util import default_context, notify_about_post, find_actor_or_create, make_image_sizes from app.chat.util import send_message from app.community.forms import SearchRemoteCommunity, CreatePostForm, ReportCommunityForm, \ DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm @@ -465,6 +465,8 @@ def add_post(actor): db.session.commit() post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}" db.session.commit() + if post.image_id and post.image.file_path is None: + make_image_sizes(post.image_id, 150, 512, 'posts') # the 512 sized image is for masonry view notify_about_post(post) diff --git a/app/community/util.py b/app/community/util.py index 44642fb0..b97b7661 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -210,13 +210,13 @@ def save_post(form, post: Post): remove_old_file(post.image_id) post.image_id = None - unused, file_extension = os.path.splitext(form.link_url.data) # do not use _ here instead of 'unused' - # this url is a link to an image - generate a thumbnail of it + unused, file_extension = os.path.splitext(form.link_url.data) + # this url is a link to an image - turn it into a image post if file_extension.lower() in allowed_extensions: - file = url_to_thumbnail_file(form.link_url.data) - if file: - post.image = file - db.session.add(file) + file = File(source_url=form.link_url.data) + post.image = file + db.session.add(file) + post.type = POST_TYPE_IMAGE else: # check opengraph tags on the page and make a thumbnail if an image is available in the og:image meta tag opengraph = opengraph_parse(form.link_url.data) @@ -270,13 +270,13 @@ def save_post(form, post: Post): img = ImageOps.exif_transpose(img) img_width = img.width img_height = img.height - if img.width > 2000 or img.height > 2000: - img.thumbnail((2000, 2000)) + if img.width > 512 or img.height > 512: + img.thumbnail((512, 512)) img.save(final_place) img_width = img.width img_height = img.height # save a second, smaller, version as a thumbnail - img.thumbnail((256, 256)) + img.thumbnail((150, 150)) img.save(final_place_thumbnail, format="WebP", quality=93) thumbnail_width = img.width thumbnail_height = img.height From e8f7551e061a611267249d5569d25219cf4fff19 Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 00:15:10 +0000 Subject: [PATCH 03/21] 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 bb3798428ef058bb2fa7fe375b6e352006188450 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 13:44:31 +1300 Subject: [PATCH 04/21] browse topics button --- app/templates/index.html | 4 +++- app/templates/list_communities.html | 1 + app/templates/search/results.html | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/templates/index.html b/app/templates/index.html index 3d195128..3e4a3ce7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -56,7 +56,9 @@ {% endfor %} -

{{ _('Explore communities') }}

+

{{ _('Explore communities') }} + {{ _('Browse topics') }} +

diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 0bc82a4e..d3e86bc1 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -105,4 +105,5 @@ {% else %}

{{ _('There are no communities yet.') }}

{% endif %} +

{{ _('Browse topics') }}

{% endblock %} diff --git a/app/templates/search/results.html b/app/templates/search/results.html index 6ce6dd07..5b7f4c92 100644 --- a/app/templates/search/results.html +++ b/app/templates/search/results.html @@ -54,7 +54,9 @@ {% endfor %} -

{{ _('Explore communities') }}

+

{{ _('Explore communities') }} + {{ _('Browse topics') }} +

From 4ae5b3645998d03f39325acaa2db775b73e51e6b Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 01:53:18 +0000 Subject: [PATCH 05/21] 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 06/21] 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 07/21] 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 08/21] 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 c5055cd09f99296af35391cfca5b1c93741d65a0 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 15:35:45 +1300 Subject: [PATCH 09/21] detect friendica --- app/activitypub/util.py | 3 +++ app/main/routes.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 239dedcf..3ea9040c 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -892,12 +892,15 @@ def refresh_instance_profile_task(instance_id: int): except requests.exceptions.JSONDecodeError as ex: instance_json = {} if 'type' in instance_json and instance_json['type'] == 'Application': + # 'name' is unreliable as the admin can change it to anything. todo: find better way if instance_json['name'].lower() == 'kbin': software = 'Kbin' elif instance_json['name'].lower() == 'mbin': software = 'Mbin' elif instance_json['name'].lower() == 'piefed': software = 'PieFed' + elif instance_json['name'].lower() == 'system account': + software = 'Friendica' else: software = 'Lemmy' instance.inbox = instance_json['inbox'] diff --git a/app/main/routes.py b/app/main/routes.py index 68eca138..cc2a69aa 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -343,8 +343,8 @@ def activitypub_application(): '@context': default_context(), 'type': 'Application', 'id': f"https://{current_app.config['SERVER_NAME']}/", - 'name': g.site.name, - 'summary': g.site.description, + 'name': 'PieFed', + 'summary': g.site.name + ' - ' + g.site.description, 'published': ap_datetime(g.site.created_at), 'updated': ap_datetime(g.site.updated), 'inbox': f"https://{current_app.config['SERVER_NAME']}/site_inbox", From caedb3e84fd6c53ebe30c11d0b423a300c40f27b Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 03:20:45 +0000 Subject: [PATCH 10/21] 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 11/21] 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 a860ce45cd61b490130e16518a622f2d580a190c Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 19:16:02 +1300 Subject: [PATCH 12/21] urllib3 version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cd6d90a0..a1b4ae00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -urllib3==1.26.1 +urllib3==1.26.11 Flask==2.3.2 python-dotenv==1.0.0 flask-wtf==1.1.1 From 39ed5d501b4dc14718b45c5e357ad0bffaf7e617 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 19:47:51 +1300 Subject: [PATCH 13/21] update documentation to avoid future migration issues fixes #113 --- INSTALL.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 8d016d19..2cd24ac0 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -120,9 +120,17 @@ If it does not work check the log file at logs/pyfedi.log for clues.
## Initialise database, and set up admin account -`flask init-db` +`export FLASK_APP=pyfedi.py` + +`flask db upgrade` + +`flask init-db` + (choose a new username, email address, and password for your PyFedi admin account) +If you see an error message "ModuleNotFoundError: No module named 'flask_babel'" then use `venv/bin/flask` instead of `flask` +for all flask commands. +
## Run the app From c0f526d70f8c4555a612de710a72947bf059d02e Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 19:48:02 +1300 Subject: [PATCH 14/21] remove unused interests --- app/cli.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/app/cli.py b/app/cli.py index 70a4a00c..7d430f98 100644 --- a/app/cli.py +++ b/app/cli.py @@ -76,15 +76,13 @@ def register(app): db.configure_mappers() db.create_all() private_key, public_key = RsaKeys.generate_keypair() - db.session.add(Site(name="PieFed", description='', public_key=public_key, private_key=private_key)) + db.session.add(Site(name="PieFed", description='Explore Anything, Discuss Everything.', public_key=public_key, private_key=private_key)) db.session.add(Instance(domain=app.config['SERVER_NAME'], software='PieFed')) # Instance 1 is always the local instance db.session.add(Settings(name='allow_nsfw', value=json.dumps(False))) db.session.add(Settings(name='allow_nsfl', value=json.dumps(False))) db.session.add(Settings(name='allow_dislike', value=json.dumps(True))) db.session.add(Settings(name='allow_local_image_posts', value=json.dumps(True))) db.session.add(Settings(name='allow_remote_image_posts', value=json.dumps(True))) - db.session.add(Settings(name='registration_open', value=json.dumps(True))) - db.session.add(Settings(name='approve_registrations', value=json.dumps(False))) db.session.add(Settings(name='federation', value=json.dumps(True))) banned_instances = ['anonib.al','lemmygrad.ml', 'gab.com', 'rqd2.net', 'exploding-heads.com', 'hexbear.net', 'threads.net', 'noauthority.social', 'pieville.net', 'links.hackliberty.org', @@ -95,21 +93,6 @@ def register(app): db.session.add(BannedInstances(domain=bi)) print("Added banned instance", bi) - print("Populating DB with instances and interests") - print("See interests.txt") - interests = file_get_contents('interests.txt') - db.session.add(Interest(name='🕊 Chilling', communities=parse_communities(interests, 'chilling'))) - db.session.add(Interest(name='💭 Interesting stuff', communities=parse_communities(interests, 'interesting stuff'))) - db.session.add(Interest(name='📰 News & Politics', communities=parse_communities(interests, 'news & politics'))) - db.session.add(Interest(name='🎮 Gaming', communities=parse_communities(interests, 'gaming'))) - db.session.add(Interest(name='🤓 Linux', communities=parse_communities(interests, 'linux'))) - db.session.add(Interest(name='♻️ Environment', communities=parse_communities(interests, 'environment'))) - db.session.add(Interest(name='🏳‍🌈 LGBTQ+', communities=parse_communities(interests, 'lgbtq'))) - db.session.add(Interest(name='🛠 Programming', communities=parse_communities(interests, 'programming'))) - db.session.add(Interest(name='🖥️ Tech', communities=parse_communities(interests, 'tech'))) - db.session.add(Interest(name='🤗 Mental Health', communities=parse_communities(interests, 'mental health'))) - db.session.add(Interest(name='💊 Health', communities=parse_communities(interests, 'health'))) - # Load initial domain block list block_list = retrieve_block_list() if block_list: From 5c5c28a4a352b453e2d262a1c33110ed82f47050 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Mar 2024 20:58:25 +1300 Subject: [PATCH 15/21] bug when editing post --- app/community/forms.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/community/forms.py b/app/community/forms.py index 5f87147f..ca80ba6c 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -136,9 +136,10 @@ class CreatePostForm(FlaskForm): current_user.reputation -= 1 db.session.commit() return False - community = Community.query.get(self.communities.data) - if community.is_local() and g.site.allow_local_image_posts is False: - self.communities.errors.append(_('Images cannot be posted to local communities.')) + if self.communities: + community = Community.query.get(self.communities.data) + if community.is_local() and g.site.allow_local_image_posts is False: + self.communities.errors.append(_('Images cannot be posted to local communities.')) elif self.post_type.data == 'poll': self.discussion_title.errors.append(_('Poll not implemented yet.')) return False From a0e974df11d288839a63040bb3ae7f6895fb98be Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 24 Mar 2024 16:38:20 +0000 Subject: [PATCH 16/21] 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 From e15d2a0581eec8111234a55bbe663ee9e8620ee1 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:30:09 +1300 Subject: [PATCH 17/21] don't accept new posts from banned communities --- app/activitypub/util.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 3ea9040c..5cab1f1a 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -227,6 +227,11 @@ def find_actor_or_create(actor: str, create_if_not_found=True, community_only=Fa return None if user is None: user = Community.query.filter(Community.ap_profile_id == actor).first() + if user and user.banned: + # Try to find a non-banned copy of the community. Sometimes duplicates happen and one copy is banned. + user = Community.query.filter(Community.ap_profile_id == actor).filter(Community.banned == False).first() + if user is None: # no un-banned version of this community exists, only the banned one. So it was banned for being bad, not for being a duplicate. + return None if user is not None: if not user.is_local() and (user.ap_fetched_at is None or user.ap_fetched_at < utcnow() - timedelta(days=7)): From fb046f1acbc6a36cb80d80c76d07cb5bedc6325f Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 25 Mar 2024 13:30:18 +1300 Subject: [PATCH 18/21] properly resize image posts --- app/community/util.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/community/util.py b/app/community/util.py index b97b7661..774cd012 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -255,6 +255,7 @@ def save_post(form, post: Post): # save the file final_place = os.path.join(directory, new_filename + file_ext) + final_place_medium = os.path.join(directory, new_filename + '_medium.webp') final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp') uploaded_file.seek(0) uploaded_file.save(final_place) @@ -270,9 +271,11 @@ def save_post(form, post: Post): img = ImageOps.exif_transpose(img) img_width = img.width img_height = img.height + img.thumbnail((2000, 2000)) + img.save(final_place) if img.width > 512 or img.height > 512: img.thumbnail((512, 512)) - img.save(final_place) + img.save(final_place_medium, format="WebP", quality=93) img_width = img.width img_height = img.height # save a second, smaller, version as a thumbnail @@ -281,7 +284,7 @@ def save_post(form, post: Post): thumbnail_width = img.width thumbnail_height = img.height - file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=alt_text, + file = File(file_path=final_place_medium, file_name=new_filename + file_ext, alt_text=alt_text, width=img_width, height=img_height, thumbnail_width=thumbnail_width, thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail, source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")) From 48bc683a6a82b71e19f2783c7d138eec7461c4c4 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:15:27 +1300 Subject: [PATCH 19/21] show users based on id --- app/user/routes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/user/routes.py b/app/user/routes.py index 91ba8cb7..bb17fd9d 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -34,6 +34,12 @@ def show_people(): joined_communities=joined_communities(current_user.get_id()), title=_('People')) +@bp.route('/user/', methods=['GET']) +def show_profile_by_id(user_id): + user = User.query.get_or_404(user_id) + return show_profile(user) + + def show_profile(user): if (user.deleted or user.banned) and current_user.is_anonymous: abort(404) From 22731c1d3879766ed5e20bd5b4d2eab6f3360e86 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:46:31 +1300 Subject: [PATCH 20/21] Add Blockquote to Each Selected Line - fixes #121 --- app/static/js/markdown/downarea.js | 6 +++++- app/templates/base.html | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/static/js/markdown/downarea.js b/app/static/js/markdown/downarea.js index 7420ecb0..4a38630c 100644 --- a/app/static/js/markdown/downarea.js +++ b/app/static/js/markdown/downarea.js @@ -582,7 +582,11 @@ var DownArea = (function () { if (self.textarea.selectionStart != self.textarea.selectionEnd) { end = self.textarea.value.substr(self.textarea.selectionEnd); var range = self.textarea.value.slice(self.textarea.selectionStart, self.textarea.selectionEnd); - blockquote = "".concat(blockquote).concat(range.trim()); + var lines = range.trim().split('\n'); + var modifiedLines = lines.map(function (line) { + return "> " + line.trim(); + }); + blockquote = modifiedLines.join('\n') + '\n'; } if (start.length && start[start.length - 1] != '\n') { blockquote = "\n".concat(blockquote); diff --git a/app/templates/base.html b/app/templates/base.html index f7b34d82..988a2aae 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -246,7 +246,7 @@ {% if post_layout == 'masonry' or post_layout == 'masonry_wide' %} {% endif %} - + {% endif %} {% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}