mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
subscription and unsubscription to remote communities - lemmy bugs
also local cache busting
This commit is contained in:
parent
5752b8eaeb
commit
d5d7122a3d
25 changed files with 174 additions and 90 deletions
|
@ -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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# pyfedi
|
||||
# PieFed
|
||||
|
||||
A lemmy/kbin clone written in Python with Flask.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
|
@ -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():
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
|
@ -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():
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
11
app/utils.py
11
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)
|
||||
|
|
32
migrations/versions/b36dac7696d1_save_moderators_url.py
Normal file
32
migrations/versions/b36dac7696d1_save_moderators_url.py
Normal 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 ###
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue