subscription and unsubscription to remote communities - lemmy bugs

also local cache busting
This commit is contained in:
rimu 2023-12-03 22:41:15 +13:00
parent 5752b8eaeb
commit d5d7122a3d
25 changed files with 174 additions and 90 deletions

View file

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

View file

@ -1,4 +1,4 @@
# pyfedi
# PieFed
A lemmy/kbin clone written in Python with Flask.

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
import logging

View file

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

View file

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

View file

@ -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 <rimu@chorebuster.net>',
send_email(_('[PieFed] Reset Your Password'),
sender='PieFed <rimu@chorebuster.net>',
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 <rimu@chorebuster.net>',
send_email(_('Welcome to PieFed'),
sender='PieFed <rimu@chorebuster.net>',
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 <rimu@chorebuster.net>',
sender='PieFed <rimu@chorebuster.net>',
recipients=[user.email],
text_body=render_template('email/verification.txt', user=user),
html_body=render_template('email/verification.html', user=user))

View file

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

View file

@ -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/<actor>', 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('/<actor>/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')

View file

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

View file

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

View file

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

View file

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

View file

@ -26,8 +26,8 @@
{{ render_field(form.discussion_body) }}
</div>
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
{{ render_field(form.link_title) }}
{{ render_field(form.link_url) }}
{{ render_field(form.link_title) }}
</div>
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
{{ render_field(form.image_title) }}
@ -39,6 +39,9 @@
</div>
{{ render_field(form.type) }}
<div class="row mt-4">
<div class="col-md-3">
{{ render_field(form.notify_author) }}
</div>
<div class="col-md-1">
{{ render_field(form.nsfw) }}
</div>
@ -49,7 +52,7 @@
</div>
</div>
{{ render_field(form.notify_author) }}
{{ render_field(form.submit) }}
</form>
</div>

View file

@ -49,8 +49,10 @@
<div class="card-body">
<div class="row">
<div class="col-6">
{% if current_user.is_authenticated and current_user.subscribed(community) %}
{% if current_user.is_authenticated and community_membership(current_user, community) == SUBSCRIPTION_MEMBER %}
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
{% elif current_user.is_authenticated and community_membership(current_user, community) == SUBSCRIPTION_PENDING %}
<a class="w-100 btn btn-outline-secondary" href="/community/{{ community.link() }}/unsubscribe">{{ _('Pending') }}</a>
{% else %}
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
{% endif %}

View file

@ -1,11 +1,11 @@
<p><a href="https://pyfedi.social/"><img src="https://pyfedi.social/images/PyFedi-logo.png" style="max-width: 100%; margin-bottom: 20px;" width="250" height="48" alt="PyFedi logo" /></a></p>
<p><a href="https://piefed.social/"><img src="https://piefed.social/images/PyFedi-logo.png" style="max-width: 100%; margin-bottom: 20px;" width="250" height="48" alt="PieFed logo" /></a></p>
<p>Hello {{ first_name }} and welcome!</p>
<p>I'm Rimu, the founder of PieFed and I'd like to personally thank you for signing up.</p>
<p>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:</p>
<ol>
<li>Helpful resources goes here</li>
</ol>
<p>I hope this helps. For more information please see <a href="https://pyfedi.social/faq.php">the FAQ</a> or ask me anything by replying to this email.</p>
<p>I hope this helps. For more information please see <a href="https://piefed.social/faq.php">the FAQ</a> or ask me anything by replying to this email.</p>
<p>Warm regards,</p>
<p>Rimu Atkinson<br />
Founder</p>

View file

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

View file

@ -50,8 +50,10 @@
<td>{{ community.post_reply_count }}</td>
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
<td>{% if current_user.is_authenticated %}
{% if current_user.subscribed(community) %}
{% if community_membership(current_user, community) == SUBSCRIPTION_MEMBER %}
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/unsubscribe">Unsubscribe</a>
{% elif community_membership(current_user, community) == SUBSCRIPTION_PENDING %}
<a class="btn btn-outline-secondary btn-sm" href="/community/{{ community.link() }}/unsubscribe">Pending</a>
{% else %}
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/subscribe">Subscribe</a>
{% endif %}

View file

@ -21,7 +21,7 @@
<div class="card-body">
<div class="row">
<div class="col-6">
{% if current_user.is_authenticated and current_user.subscribed(post.community) %}
{% if current_user.is_authenticated and community_membership(current_user, post.community) %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
{% else %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>

View file

@ -61,7 +61,7 @@
<div class="card-body">
<div class="row">
<div class="col-6">
{% if current_user.is_authenticated and current_user.subscribed(post.community) %}
{% if current_user.is_authenticated and community_membership(current_user, post.community) %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
{% else %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>

View file

@ -112,7 +112,7 @@
<div class="card-body">
<div class="row">
<div class="col-6">
{% if current_user.is_authenticated and current_user.subscribed(post.community) %}
{% if current_user.is_authenticated and community_membership(current_user, post.community) %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
{% else %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>

View file

@ -50,8 +50,8 @@
{{ render_field(form.discussion_body) }}
</div>
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
{{ render_field(form.link_title) }}
{{ render_field(form.link_url) }}
{{ render_field(form.link_title) }}
</div>
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
{{ render_field(form.image_title) }}
@ -63,17 +63,21 @@
</div>
{{ render_field(form.type) }}
<div class="row mt-4">
<div class="col-md-3">
{{ render_field(form.notify_author) }}
</div>
<div class="col-md-1">
{{ render_field(form.nsfw) }}
</div>
<div class="col-md-1">
{{ render_field(form.nsfl) }}
</div>
<div class="col">
</div>
</div>
{{ render_field(form.notify_author) }}
{{ render_field(form.submit) }}
</form>
</div>

View file

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

View file

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

View file

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