From d5d7122a3da61865312b48c26aaf8a81ef7b95ab Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 3 Dec 2023 22:41:15 +1300 Subject: [PATCH] subscription and unsubscription to remote communities - lemmy bugs also local cache busting --- CONTRIBUTING.md | 8 +-- README.md | 2 +- ROADMAP.md | 2 +- app/__init__.py | 2 +- app/activitypub/routes.py | 29 ++++++---- app/activitypub/signature.py | 31 +--------- app/auth/email.py | 10 ++-- app/community/forms.py | 2 +- app/community/routes.py | 57 +++++++++++++++---- app/community/util.py | 1 + app/constants.py | 3 +- app/main/routes.py | 18 ++++-- app/models.py | 18 +++--- app/templates/community/add_post.html | 7 ++- app/templates/community/community.html | 4 +- app/templates/email/welcome.html | 4 +- app/templates/email/welcome.txt | 2 +- app/templates/list_communities.html | 4 +- app/templates/post/add_reply.html | 2 +- app/templates/post/continue_discussion.html | 2 +- app/templates/post/post.html | 2 +- app/templates/post/post_edit.html | 8 ++- app/utils.py | 11 +++- .../b36dac7696d1_save_moderators_url.py | 32 +++++++++++ pyfedi.py | 3 +- 25 files changed, 174 insertions(+), 90 deletions(-) create mode 100644 migrations/versions/b36dac7696d1_save_moderators_url.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42742132..59a528e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to PyFedi +# Contributing to PieFed -When it matures enough, PyFedi will aim to work in a way consistent with the [Collective Code Construction Contract](https://42ity.org/c4.html). +When it matures enough, PieFed will aim to work in a way consistent with the [Collective Code Construction Contract](https://42ity.org/c4.html). Please discuss your ideas in an issue at https://codeberg.org/rimu/pyfedi/issues before starting any large pieces of work to ensure alignment with the roadmap, architecture and processes. @@ -58,7 +58,7 @@ Changes to this file are turned into changes in the DB by using '[migrations](ht ## Our Pledge -We, the contributors and maintainers of the PyFedi project, pledge to create a welcoming and harassment-free environment for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We, the contributors and maintainers of the PieFed project, pledge to create a welcoming and harassment-free environment for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards @@ -102,4 +102,4 @@ This Code of Conduct is adapted from the [Contributor Covenant](https://www.cont ## Conclusion -We all share the responsibility of upholding these standards. Let's work together to create a welcoming and inclusive environment that fosters collaboration, learning, and the growth of the PyFedi community. +We all share the responsibility of upholding these standards. Let's work together to create a welcoming and inclusive environment that fosters collaboration, learning, and the growth of the PieFed community. diff --git a/README.md b/README.md index 3f88ba88..90b875d8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# pyfedi +# PieFed A lemmy/kbin clone written in Python with Flask. diff --git a/ROADMAP.md b/ROADMAP.md index 38bebf05..24928b30 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,4 @@ -# PyFedi Roadmap +# PieFed Roadmap The following are the goals for a 1.0 release, good enough for production use. Items with a ✅ are complete. diff --git a/app/__init__.py b/app/__init__.py index 3c3475f0..9a8a0546 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ -# This file is part of pyfedi, which is licensed under the GNU General Public License (GPL) version 3.0. +# This file is part of PieFed, which is licensed under the GNU General Public License (GPL) version 3.0. # You should have received a copy of the GPL along with this program. If not, see . import logging diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index defeeac2..9fa56ded 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -1,20 +1,20 @@ from datetime import datetime -from app import db, constants +from app import db, constants, cache from app.activitypub import bp from flask import request, Response, current_app, abort, jsonify, json from app.activitypub.signature import HttpSignature from app.community.routes import show_community from app.user.routes import show_profile -from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE +from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, SUBSCRIPTION_MEMBER from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan, ActivityPubLog, Post, \ PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \ post_to_activity, find_actor_or_create, default_context, instance_blocked, find_reply_parent, find_liked_object, \ lemmy_site_data, instance_weight from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \ - domain_from_url, markdown_to_html + domain_from_url, markdown_to_html, community_membership import werkzeug.exceptions INBOX = [] @@ -143,7 +143,7 @@ def user_profile(actor): user = User.query.filter_by(user_name=actor, deleted=False, banned=False, ap_id=None).first() if user is not None: - if 'application/ld+json' in request.headers.get('Accept', ''): + if 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', ''): server = current_app.config['SERVER_NAME'] actor_data = { "@context": default_context(), "type": "Person", @@ -154,12 +154,12 @@ def user_profile(actor): "publicKey": { "id": f"https://{server}/u/{actor}#main-key", "owner": f"https://{server}/u/{actor}", - "publicKeyPem": user.public_key.replace("\n", "\\n") + "publicKeyPem": user.public_key # .replace("\n", "\\n") #LOOKSWRONG }, "endpoints": { "sharedInbox": f"https://{server}/inbox" }, - "published": user.created.isoformat(), + "published": user.created.isoformat() + '+00:00', } if user.avatar_id is not None: actor_data["icon"] = { @@ -188,7 +188,7 @@ def community_profile(actor): actor = actor.strip() if '@' in actor: # don't provide activitypub info for remote communities - if 'application/ld+json' in request.headers.get('Accept', ''): + if 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', ''): abort(404) community: Community = Community.query.filter_by(ap_id=actor, banned=False).first() else: @@ -374,7 +374,7 @@ def shared_inbox(): else: activity_log.exception_message = 'Unacceptable type (kbin): ' + object_type - # Announce is new content and votes, lemmy style + # Announce is new content and votes, mastodon style (?) if request_json['type'] == 'Announce': if request_json['object']['type'] == 'Create': activity_log.activity_type = request_json['object']['type'] @@ -473,7 +473,9 @@ def shared_inbox(): vote_weight = instance_weight(user.ap_domain) liked = find_liked_object(liked_ap_id) # insert into voted table - if liked is not None and isinstance(liked, Post): + if liked is None: + activity_log.exception_message = 'Liked object not found' + elif liked is not None and isinstance(liked, Post): existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first() if existing_vote: existing_vote.effect = vote_effect * vote_weight @@ -499,7 +501,7 @@ def shared_inbox(): ... # todo: recalculate 'hotness' of liked post/reply # todo: if vote was on content in local community, federate the vote out to followers - # Follow: remote user wants to follow one of our communities + # Follow: remote user wants to join/follow one of our communities elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community user_ap_id = request_json['actor'] community_ap_id = request_json['object'] @@ -511,10 +513,11 @@ def shared_inbox(): banned = CommunityBan.query.filter_by(user_id=user.id, community_id=community.id).first() if banned is None: - if not user.subscribed(community): + if community_membership(user, community) != SUBSCRIPTION_MEMBER: member = CommunityMember(user_id=user.id, community_id=community.id) db.session.add(member) db.session.commit() + cache.delete_memoized(community_membership, user, community) # send accept message to acknowledge the follow accept = { "@context": default_context(), @@ -561,9 +564,11 @@ def shared_inbox(): community.subscriptions_count += 1 db.session.commit() activity_log.result = 'success' + cache.delete_memoized(community_membership, user, community) + elif request_json['type'] == 'Undo': if request_json['object']['type'] == 'Follow': # Unsubscribe from a community - community_ap_id = request_json['actor'] + community_ap_id = request_json['object']['object'] user_ap_id = request_json['object']['actor'] user = find_actor_or_create(user_ap_id) community = find_actor_or_create(community_ap_id) diff --git a/app/activitypub/signature.py b/app/activitypub/signature.py index 015c1355..16b49c04 100644 --- a/app/activitypub/signature.py +++ b/app/activitypub/signature.py @@ -44,6 +44,7 @@ from datetime import datetime from dateutil import parser from pyld import jsonld +from app.activitypub.util import default_context from app.constants import DATETIME_MS_FORMAT @@ -245,7 +246,7 @@ class HttpSignature: body: dict | None, private_key: str, key_id: str, - content_type: str = "application/json", + content_type: str = "application/activity+json", method: Literal["get", "post"] = "post", timeout: int = 5, ): @@ -266,33 +267,7 @@ class HttpSignature: # If we have a body, add a digest and content type if body is not None: if '@context' not in body: # add a default json-ld context if necessary - body['@context'] = [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - { - "piefed": "https://piefed.social/ns#", - "lemmy": "https://join-lemmy.org/ns#", - "litepub": "http://litepub.social/ns#", - "pt": "https://joinpeertube.org/ns#", - "sc": "http://schema.org/", - "nsfl": "piefed:nsfl", - "ChatMessage": "litepub:ChatMessage", - "commentsEnabled": "pt:commentsEnabled", - "sensitive": "as:sensitive", - "matrixUserId": "lemmy:matrixUserId", - "postingRestrictedToMods": "lemmy:postingRestrictedToMods", - "removeData": "lemmy:removeData", - "stickied": "lemmy:stickied", - "moderators": { - "@type": "@id", - "@id": "lemmy:moderators" - }, - "expires": "as:endTime", - "distinguished": "lemmy:distinguished", - "language": "sc:inLanguage", - "identifier": "sc:identifier" - } - ] + body['@context'] = default_context() body_bytes = json.dumps(body).encode("utf8") headers["Digest"] = cls.calculate_digest(body_bytes) headers["Content-Type"] = content_type diff --git a/app/auth/email.py b/app/auth/email.py index 3c24f91b..ab718db8 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -5,8 +5,8 @@ from app.email import send_email def send_password_reset_email(user): token = user.get_reset_password_token() - send_email(_('[PyFedi] Reset Your Password'), - sender='PyFedi ', + send_email(_('[PieFed] Reset Your Password'), + sender='PieFed ', recipients=[user.email], text_body=render_template('email/reset_password.txt', user=user, token=token), @@ -15,8 +15,8 @@ def send_password_reset_email(user): def send_welcome_email(user): - send_email(_('Welcome to PyFedi'), - sender='PyFedi ', + send_email(_('Welcome to PieFed'), + sender='PieFed ', recipients=[user.email], text_body=render_template('email/welcome.txt', user=user), html_body=render_template('email/welcome.html', user=user)) @@ -24,7 +24,7 @@ def send_welcome_email(user): def send_verification_email(user): send_email(_('Please verify your email address'), - sender='PyFedi ', + sender='PieFed ', recipients=[user.email], text_body=render_template('email/verification.txt', user=user), html_body=render_template('email/verification.html', user=user)) \ No newline at end of file diff --git a/app/community/forms.py b/app/community/forms.py index c4c18162..cf1f9d68 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -45,7 +45,7 @@ class CreatePostForm(FlaskForm): nsfw = BooleanField(_l('NSFW')) nsfl = BooleanField(_l('NSFL')) notify_author = BooleanField(_l('Notify about replies')) - submit = SubmitField(_l('Post')) + submit = SubmitField(_l('Save')) def validate(self, extra_validators=None) -> bool: if not super().validate(): diff --git a/app/community/routes.py b/app/community/routes.py index 0c2be1f5..78709237 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -4,17 +4,19 @@ from flask_babel import _ from pillow_heif import register_heif_opener from sqlalchemy import or_, desc -from app import db, constants +from app import db, constants, cache from app.activitypub.signature import RsaKeys, HttpSignature +from app.activitypub.util import default_context from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm from app.community.util import search_for_community, community_url_exists, actor_to_community, \ ensure_directory_exists, opengraph_parse, url_to_thumbnail_file, save_post -from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE +from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \ + SUBSCRIPTION_PENDING from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \ File, PostVote from app.community import bp from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \ - shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish + shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership import os from PIL import Image, ImageOps from datetime import datetime @@ -70,7 +72,7 @@ def add_remote(): return render_template('community/add_remote.html', title=_('Add remote community'), form=form, new_community=new_community, - subscribed=current_user.subscribed(new_community) >= SUBSCRIPTION_MEMBER) + subscribed=community_membership(current_user, new_community) >= SUBSCRIPTION_MEMBER) # @bp.route('/c/', methods=['GET']) - defined in activitypub/routes.py, which calls this function for user requests. A bit weird. @@ -97,7 +99,8 @@ def show_community(community: Community): return render_template('community/community.html', community=community, title=community.title, is_moderator=is_moderator, is_owner=is_owner, mods=mod_list, posts=posts, description=description, - og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK) + og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, + SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER) @bp.route('//subscribe', methods=['GET']) @@ -113,7 +116,7 @@ def subscribe(actor): community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first() if community is not None: - if not current_user.subscribed(community): + if community_membership(current_user, community) != SUBSCRIPTION_MEMBER and community_membership(current_user, community) != SUBSCRIPTION_PENDING: if remote: # send ActivityPub message to remote community, asking to follow. Accept message will be sent to our shared inbox join_request = CommunityJoinRequest(user_id=current_user.id, community_id=community.id) @@ -161,12 +164,46 @@ def unsubscribe(actor): community = actor_to_community(actor) if community is not None: - subscription = current_user.subscribed(community) + subscription = community_membership(current_user, community) if subscription: if subscription != SUBSCRIPTION_OWNER: - db.session.query(CommunityMember).filter_by(user_id=current_user.id, community_id=community.id).delete() - db.session.commit() - flash('You are unsubscribed from ' + community.title) + proceed = True + # Undo the Follow + if '@' in actor: # this is a remote community, so activitypub is needed + follow = { + "actor": f"https://{current_app.config['SERVER_NAME']}/u/{current_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/{gibberish(15)}" + } + undo = { + 'actor': current_user.profile_id(), + 'to': [community.ap_profile_id], + 'type': 'Undo', + 'id': f"https://{current_app.config['SERVER_NAME']}/activities/undo/" + gibberish(15), + 'object': follow + } + try: + message = HttpSignature.signed_request(community.ap_inbox_url, undo, current_user.private_key, + current_user.profile_id() + '#main-key') + if message.status_code != 200: + flash('Response status code was not 200', 'warning') + current_app.logger.error('Response code for unsubscription attempt was ' + + str(message.status_code) + ' ' + message.text) + proceed = False + except Exception as ex: + proceed = False + flash('Failed to send request to unsubscribe: ' + str(ex), 'error') + current_app.logger.error("Exception while trying to unsubscribe" + str(ex)) + if proceed: + db.session.query(CommunityMember).filter_by(user_id=current_user.id, community_id=community.id).delete() + db.session.query(CommunityJoinRequest).filter_by(user_id=current_user.id, community_id=community.id).delete() + db.session.commit() + + flash('You are unsubscribed from ' + community.title) + cache.delete_memoized(community_membership, current_user, community) + else: # todo: community deletion flash('You need to make someone else the owner before unsubscribing.', 'warning') diff --git a/app/community/util.py b/app/community/util.py index e3452875..67bdec83 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -56,6 +56,7 @@ def search_for_community(address: str): ap_profile_id=community_json['id'], ap_followers_url=community_json['followers'], ap_inbox_url=community_json['endpoints']['sharedInbox'], + ap_moderators_url=community_json['attributedTo'] if 'attributedTo' in community_json else None, ap_fetched_at=datetime.utcnow(), ap_domain=server, public_key=community_json['publicKey']['publicKeyPem'], diff --git a/app/constants.py b/app/constants.py index d57183a7..97001e31 100644 --- a/app/constants.py +++ b/app/constants.py @@ -13,6 +13,7 @@ SUBSCRIPTION_OWNER = 3 SUBSCRIPTION_MODERATOR = 2 SUBSCRIPTION_MEMBER = 1 SUBSCRIPTION_NONMEMBER = 0 -SUBSCRIPTION_BANNED = -1 +SUBSCRIPTION_PENDING = -1 +SUBSCRIPTION_BANNED = -2 THREAD_CUTOFF_DEPTH = 4 \ No newline at end of file diff --git a/app/main/routes.py b/app/main/routes.py index e2e55ebc..56b7e96c 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,5 +1,6 @@ -from app import db +from app import db, cache +from app.constants import SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER from app.main import bp from flask import g, session, flash, request from flask_moment import moment @@ -8,7 +9,6 @@ from flask_babel import _, get_locale from sqlalchemy import select from sqlalchemy_searchable import search from app.utils import render_template, get_setting, gibberish - from app.models import Community, CommunityMember @@ -29,21 +29,29 @@ def list_communities(): query = search(select(Community), search_param, sort=True) communities = db.session.scalars(query).all() - return render_template('list_communities.html', communities=communities, search=search_param, title=_('Communities')) + return render_template('list_communities.html', communities=communities, search=search_param, title=_('Communities'), + SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER) @bp.route('/communities/local', methods=['GET']) def list_local_communities(): verification_warning() communities = Community.query.filter_by(ap_id=None, banned=False).all() - return render_template('list_communities.html', communities=communities, title=_('Local communities')) + return render_template('list_communities.html', communities=communities, title=_('Local communities'), + SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER) @bp.route('/communities/subscribed', methods=['GET']) def list_subscribed_communities(): verification_warning() communities = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == current_user.id).all() - return render_template('list_communities.html', communities=communities, title=_('Subscribed communities')) + return render_template('list_communities.html', communities=communities, title=_('Subscribed communities'), + SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER) + + +@bp.route('/test') +def test(): + ... def verification_warning(): diff --git a/app/models.py b/app/models.py index f83d9fd0..56bdfd9f 100644 --- a/app/models.py +++ b/app/models.py @@ -17,7 +17,7 @@ import jwt import os from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER, \ - SUBSCRIPTION_BANNED + SUBSCRIPTION_BANNED, SUBSCRIPTION_PENDING class FullTextSearchQuery(BaseQuery, SearchQueryMixin): @@ -87,6 +87,7 @@ class Community(db.Model): ap_fetched_at = db.Column(db.DateTime) ap_deleted_at = db.Column(db.DateTime) ap_inbox_url = db.Column(db.String(255)) + ap_moderators_url = db.Column(db.String(255)) ap_domain = db.Column(db.String(255)) banned = db.Column(db.Boolean, default=False) @@ -276,11 +277,10 @@ class User(UserMixin, db.Model): return True return self.expires < datetime(2019, 9, 1) - @cache.memoize(timeout=50) - def subscribed(self, community: Community) -> int: - if community is None: + def subscribed(self, community_id: int) -> int: + if community_id is None: return False - subscription:CommunityMember = CommunityMember.query.filter_by(user_id=self.id, community_id=community.id).first() + subscription:CommunityMember = CommunityMember.query.filter_by(user_id=self.id, community_id=community_id).first() if subscription: if subscription.is_banned: return SUBSCRIPTION_BANNED @@ -291,7 +291,11 @@ class User(UserMixin, db.Model): else: return SUBSCRIPTION_MEMBER else: - return SUBSCRIPTION_NONMEMBER + join_request = CommunityJoinRequest.query.filter_by(user_id=self.id, community_id=community_id).first() + if join_request: + return SUBSCRIPTION_PENDING + else: + return SUBSCRIPTION_NONMEMBER def communities(self) -> List[Community]: return Community.query.filter(Community.banned == False).\ @@ -388,7 +392,7 @@ class Post(db.Model): image = db.relationship(File, lazy='joined', foreign_keys=[image_id], cascade="all, delete") domain = db.relationship('Domain', lazy='joined', foreign_keys=[domain_id]) - author = db.relationship('User', lazy='joined', foreign_keys=[user_id]) + author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id]) @classmethod def get_by_ap_id(cls, ap_id): diff --git a/app/templates/community/add_post.html b/app/templates/community/add_post.html index 9da78b46..2c303507 100644 --- a/app/templates/community/add_post.html +++ b/app/templates/community/add_post.html @@ -26,8 +26,8 @@ {{ render_field(form.discussion_body) }}
{{ render_field(form.image_title) }} @@ -39,6 +39,9 @@
{{ render_field(form.type) }}
+
+ {{ render_field(form.notify_author) }} +
{{ render_field(form.nsfw) }}
@@ -49,7 +52,7 @@
- {{ render_field(form.notify_author) }} + {{ render_field(form.submit) }} diff --git a/app/templates/community/community.html b/app/templates/community/community.html index 0114a4d6..d3670d73 100644 --- a/app/templates/community/community.html +++ b/app/templates/community/community.html @@ -49,8 +49,10 @@
- {% if current_user.is_authenticated and current_user.subscribed(community) %} + {% if current_user.is_authenticated and community_membership(current_user, community) == SUBSCRIPTION_MEMBER %} {{ _('Unsubscribe') }} + {% elif current_user.is_authenticated and community_membership(current_user, community) == SUBSCRIPTION_PENDING %} + {{ _('Pending') }} {% else %} {{ _('Subscribe') }} {% endif %} diff --git a/app/templates/email/welcome.html b/app/templates/email/welcome.html index cd628c8d..e63a12d4 100644 --- a/app/templates/email/welcome.html +++ b/app/templates/email/welcome.html @@ -1,11 +1,11 @@ -

PyFedi logo

+

PieFed logo

Hello {{ first_name }} and welcome!

I'm Rimu, the founder of PieFed and I'd like to personally thank you for signing up.

I'd also like to give you the tools and information you need to get the most out of it. Let's get you up and running:

  1. Helpful resources goes here
-

I hope this helps. For more information please see the FAQ or ask me anything by replying to this email.

+

I hope this helps. For more information please see the FAQ or ask me anything by replying to this email.

Warm regards,

Rimu Atkinson
Founder

diff --git a/app/templates/email/welcome.txt b/app/templates/email/welcome.txt index 0ed8290b..64e26833 100644 --- a/app/templates/email/welcome.txt +++ b/app/templates/email/welcome.txt @@ -6,7 +6,7 @@ I'd also like to give you the tools and information you need to get the most out 1. Helpful resources go here -I hope this helps. For more information please see the FAQ at https://pyfedi.social/faq.php or ask me anything by replying to this email. +I hope this helps. For more information please see the FAQ at https://piefed.social/faq.php or ask me anything by replying to this email. Warm regards, diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index 01f93172..35dfaabd 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -50,8 +50,10 @@ {{ community.post_reply_count }} {{ moment(community.last_active).fromNow(refresh=True) }} {% if current_user.is_authenticated %} - {% if current_user.subscribed(community) %} + {% if community_membership(current_user, community) == SUBSCRIPTION_MEMBER %} Unsubscribe + {% elif community_membership(current_user, community) == SUBSCRIPTION_PENDING %} + Pending {% else %} Subscribe {% endif %} diff --git a/app/templates/post/add_reply.html b/app/templates/post/add_reply.html index 0a762aeb..bdbe6413 100644 --- a/app/templates/post/add_reply.html +++ b/app/templates/post/add_reply.html @@ -21,7 +21,7 @@
- {% if current_user.is_authenticated and current_user.subscribed(post.community) %} + {% if current_user.is_authenticated and community_membership(current_user, post.community) %} {{ _('Unsubscribe') }} {% else %} {{ _('Subscribe') }} diff --git a/app/templates/post/continue_discussion.html b/app/templates/post/continue_discussion.html index 2ffb9f81..f72fcd2d 100644 --- a/app/templates/post/continue_discussion.html +++ b/app/templates/post/continue_discussion.html @@ -61,7 +61,7 @@
- {% if current_user.is_authenticated and current_user.subscribed(post.community) %} + {% if current_user.is_authenticated and community_membership(current_user, post.community) %} {{ _('Unsubscribe') }} {% else %} {{ _('Subscribe') }} diff --git a/app/templates/post/post.html b/app/templates/post/post.html index 4a41dc13..3b2a8757 100644 --- a/app/templates/post/post.html +++ b/app/templates/post/post.html @@ -112,7 +112,7 @@
- {% if current_user.is_authenticated and current_user.subscribed(post.community) %} + {% if current_user.is_authenticated and community_membership(current_user, post.community) %} {{ _('Unsubscribe') }} {% else %} {{ _('Subscribe') }} diff --git a/app/templates/post/post_edit.html b/app/templates/post/post_edit.html index 44935063..2bcd5338 100644 --- a/app/templates/post/post_edit.html +++ b/app/templates/post/post_edit.html @@ -50,8 +50,8 @@ {{ render_field(form.discussion_body) }}
{{ render_field(form.image_title) }} @@ -63,17 +63,21 @@
{{ render_field(form.type) }}
+
+ {{ render_field(form.notify_author) }} +
{{ render_field(form.nsfw) }}
{{ render_field(form.nsfl) }}
+
- {{ render_field(form.notify_author) }} + {{ render_field(form.submit) }}
diff --git a/app/utils.py b/app/utils.py index 8050a32a..d3e9be76 100644 --- a/app/utils.py +++ b/app/utils.py @@ -14,7 +14,7 @@ from flask_login import current_user from sqlalchemy import text from app import db, cache -from app.models import Settings, Domain, Instance, BannedInstances, User +from app.models import Settings, Domain, Instance, BannedInstances, User, Community # Flask's render_template function, with support for themes added @@ -198,6 +198,15 @@ def user_access(permission: str, user_id: int) -> bool: return has_access is not None +@cache.memoize(timeout=10) +def community_membership(user: User, community: Community) -> int: + # @cache.memoize works with User.subscribed but cache.delete_memoized does not, making it bad to use on class methods. + # however cache.memoize and cache.delete_memoized works fine with normal functions + if community is None: + return False + return user.subscribed(community.id) + + def retrieve_block_list(): try: response = requests.get('https://github.com/rimu/no-qanon/blob/master/domains.txt', timeout=1) diff --git a/migrations/versions/b36dac7696d1_save_moderators_url.py b/migrations/versions/b36dac7696d1_save_moderators_url.py new file mode 100644 index 00000000..0d52cb0a --- /dev/null +++ b/migrations/versions/b36dac7696d1_save_moderators_url.py @@ -0,0 +1,32 @@ +"""save moderators url + +Revision ID: b36dac7696d1 +Revises: cae2e31293e8 +Create Date: 2023-12-01 13:02:31.139428 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'b36dac7696d1' +down_revision = 'cae2e31293e8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.add_column(sa.Column('ap_moderators_url', sa.String(length=255), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.drop_column('ap_moderators_url') + + # ### end Alembic commands ### diff --git a/pyfedi.py b/pyfedi.py index c8e4db7f..9762bc9d 100644 --- a/pyfedi.py +++ b/pyfedi.py @@ -6,7 +6,7 @@ from app import create_app, db, cli import os, click from flask import session, g, json from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE -from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access +from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership app = create_app() cli.register(app) @@ -29,6 +29,7 @@ with app.app_context(): app.jinja_env.globals['len'] = len app.jinja_env.globals['digits'] = digits app.jinja_env.globals['str'] = str + app.jinja_env.globals['community_membership'] = community_membership app.jinja_env.globals['json_loads'] = json.loads app.jinja_env.globals['user_access'] = user_access app.jinja_env.filters['shorten'] = shorten_string