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
|
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.
|
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
|
## 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
|
## Our Standards
|
||||||
|
|
||||||
|
@ -102,4 +102,4 @@ This Code of Conduct is adapted from the [Contributor Covenant](https://www.cont
|
||||||
|
|
||||||
## Conclusion
|
## 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.
|
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.
|
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/>.
|
# You should have received a copy of the GPL along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from app import db, constants
|
from app import db, constants, cache
|
||||||
from app.activitypub import bp
|
from app.activitypub import bp
|
||||||
from flask import request, Response, current_app, abort, jsonify, json
|
from flask import request, Response, current_app, abort, jsonify, json
|
||||||
|
|
||||||
from app.activitypub.signature import HttpSignature
|
from app.activitypub.signature import HttpSignature
|
||||||
from app.community.routes import show_community
|
from app.community.routes import show_community
|
||||||
from app.user.routes import show_profile
|
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, \
|
from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan, ActivityPubLog, Post, \
|
||||||
PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances
|
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, \
|
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, \
|
post_to_activity, find_actor_or_create, default_context, instance_blocked, find_reply_parent, find_liked_object, \
|
||||||
lemmy_site_data, instance_weight
|
lemmy_site_data, instance_weight
|
||||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
|
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
|
import werkzeug.exceptions
|
||||||
|
|
||||||
INBOX = []
|
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()
|
user = User.query.filter_by(user_name=actor, deleted=False, banned=False, ap_id=None).first()
|
||||||
|
|
||||||
if user is not None:
|
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']
|
server = current_app.config['SERVER_NAME']
|
||||||
actor_data = { "@context": default_context(),
|
actor_data = { "@context": default_context(),
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
|
@ -154,12 +154,12 @@ def user_profile(actor):
|
||||||
"publicKey": {
|
"publicKey": {
|
||||||
"id": f"https://{server}/u/{actor}#main-key",
|
"id": f"https://{server}/u/{actor}#main-key",
|
||||||
"owner": f"https://{server}/u/{actor}",
|
"owner": f"https://{server}/u/{actor}",
|
||||||
"publicKeyPem": user.public_key.replace("\n", "\\n")
|
"publicKeyPem": user.public_key # .replace("\n", "\\n") #LOOKSWRONG
|
||||||
},
|
},
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"sharedInbox": f"https://{server}/inbox"
|
"sharedInbox": f"https://{server}/inbox"
|
||||||
},
|
},
|
||||||
"published": user.created.isoformat(),
|
"published": user.created.isoformat() + '+00:00',
|
||||||
}
|
}
|
||||||
if user.avatar_id is not None:
|
if user.avatar_id is not None:
|
||||||
actor_data["icon"] = {
|
actor_data["icon"] = {
|
||||||
|
@ -188,7 +188,7 @@ def community_profile(actor):
|
||||||
actor = actor.strip()
|
actor = actor.strip()
|
||||||
if '@' in actor:
|
if '@' in actor:
|
||||||
# don't provide activitypub info for remote communities
|
# 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)
|
abort(404)
|
||||||
community: Community = Community.query.filter_by(ap_id=actor, banned=False).first()
|
community: Community = Community.query.filter_by(ap_id=actor, banned=False).first()
|
||||||
else:
|
else:
|
||||||
|
@ -374,7 +374,7 @@ def shared_inbox():
|
||||||
else:
|
else:
|
||||||
activity_log.exception_message = 'Unacceptable type (kbin): ' + object_type
|
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['type'] == 'Announce':
|
||||||
if request_json['object']['type'] == 'Create':
|
if request_json['object']['type'] == 'Create':
|
||||||
activity_log.activity_type = request_json['object']['type']
|
activity_log.activity_type = request_json['object']['type']
|
||||||
|
@ -473,7 +473,9 @@ def shared_inbox():
|
||||||
vote_weight = instance_weight(user.ap_domain)
|
vote_weight = instance_weight(user.ap_domain)
|
||||||
liked = find_liked_object(liked_ap_id)
|
liked = find_liked_object(liked_ap_id)
|
||||||
# insert into voted table
|
# 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()
|
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first()
|
||||||
if existing_vote:
|
if existing_vote:
|
||||||
existing_vote.effect = vote_effect * vote_weight
|
existing_vote.effect = vote_effect * vote_weight
|
||||||
|
@ -499,7 +501,7 @@ def shared_inbox():
|
||||||
... # todo: recalculate 'hotness' of liked post/reply
|
... # todo: recalculate 'hotness' of liked post/reply
|
||||||
# todo: if vote was on content in local community, federate the vote out to followers
|
# 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
|
elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community
|
||||||
user_ap_id = request_json['actor']
|
user_ap_id = request_json['actor']
|
||||||
community_ap_id = request_json['object']
|
community_ap_id = request_json['object']
|
||||||
|
@ -511,10 +513,11 @@ def shared_inbox():
|
||||||
banned = CommunityBan.query.filter_by(user_id=user.id,
|
banned = CommunityBan.query.filter_by(user_id=user.id,
|
||||||
community_id=community.id).first()
|
community_id=community.id).first()
|
||||||
if banned is None:
|
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)
|
member = CommunityMember(user_id=user.id, community_id=community.id)
|
||||||
db.session.add(member)
|
db.session.add(member)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
cache.delete_memoized(community_membership, user, community)
|
||||||
# send accept message to acknowledge the follow
|
# send accept message to acknowledge the follow
|
||||||
accept = {
|
accept = {
|
||||||
"@context": default_context(),
|
"@context": default_context(),
|
||||||
|
@ -561,9 +564,11 @@ def shared_inbox():
|
||||||
community.subscriptions_count += 1
|
community.subscriptions_count += 1
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
activity_log.result = 'success'
|
activity_log.result = 'success'
|
||||||
|
cache.delete_memoized(community_membership, user, community)
|
||||||
|
|
||||||
elif request_json['type'] == 'Undo':
|
elif request_json['type'] == 'Undo':
|
||||||
if request_json['object']['type'] == 'Follow': # Unsubscribe from a community
|
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_ap_id = request_json['object']['actor']
|
||||||
user = find_actor_or_create(user_ap_id)
|
user = find_actor_or_create(user_ap_id)
|
||||||
community = find_actor_or_create(community_ap_id)
|
community = find_actor_or_create(community_ap_id)
|
||||||
|
|
|
@ -44,6 +44,7 @@ from datetime import datetime
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
from pyld import jsonld
|
from pyld import jsonld
|
||||||
|
|
||||||
|
from app.activitypub.util import default_context
|
||||||
from app.constants import DATETIME_MS_FORMAT
|
from app.constants import DATETIME_MS_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
@ -245,7 +246,7 @@ class HttpSignature:
|
||||||
body: dict | None,
|
body: dict | None,
|
||||||
private_key: str,
|
private_key: str,
|
||||||
key_id: str,
|
key_id: str,
|
||||||
content_type: str = "application/json",
|
content_type: str = "application/activity+json",
|
||||||
method: Literal["get", "post"] = "post",
|
method: Literal["get", "post"] = "post",
|
||||||
timeout: int = 5,
|
timeout: int = 5,
|
||||||
):
|
):
|
||||||
|
@ -266,33 +267,7 @@ class HttpSignature:
|
||||||
# If we have a body, add a digest and content type
|
# If we have a body, add a digest and content type
|
||||||
if body is not None:
|
if body is not None:
|
||||||
if '@context' not in body: # add a default json-ld context if necessary
|
if '@context' not in body: # add a default json-ld context if necessary
|
||||||
body['@context'] = [
|
body['@context'] = default_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_bytes = json.dumps(body).encode("utf8")
|
body_bytes = json.dumps(body).encode("utf8")
|
||||||
headers["Digest"] = cls.calculate_digest(body_bytes)
|
headers["Digest"] = cls.calculate_digest(body_bytes)
|
||||||
headers["Content-Type"] = content_type
|
headers["Content-Type"] = content_type
|
||||||
|
|
|
@ -5,8 +5,8 @@ from app.email import send_email
|
||||||
|
|
||||||
def send_password_reset_email(user):
|
def send_password_reset_email(user):
|
||||||
token = user.get_reset_password_token()
|
token = user.get_reset_password_token()
|
||||||
send_email(_('[PyFedi] Reset Your Password'),
|
send_email(_('[PieFed] Reset Your Password'),
|
||||||
sender='PyFedi <rimu@chorebuster.net>',
|
sender='PieFed <rimu@chorebuster.net>',
|
||||||
recipients=[user.email],
|
recipients=[user.email],
|
||||||
text_body=render_template('email/reset_password.txt',
|
text_body=render_template('email/reset_password.txt',
|
||||||
user=user, token=token),
|
user=user, token=token),
|
||||||
|
@ -15,8 +15,8 @@ def send_password_reset_email(user):
|
||||||
|
|
||||||
|
|
||||||
def send_welcome_email(user):
|
def send_welcome_email(user):
|
||||||
send_email(_('Welcome to PyFedi'),
|
send_email(_('Welcome to PieFed'),
|
||||||
sender='PyFedi <rimu@chorebuster.net>',
|
sender='PieFed <rimu@chorebuster.net>',
|
||||||
recipients=[user.email],
|
recipients=[user.email],
|
||||||
text_body=render_template('email/welcome.txt', user=user),
|
text_body=render_template('email/welcome.txt', user=user),
|
||||||
html_body=render_template('email/welcome.html', 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):
|
def send_verification_email(user):
|
||||||
send_email(_('Please verify your email address'),
|
send_email(_('Please verify your email address'),
|
||||||
sender='PyFedi <rimu@chorebuster.net>',
|
sender='PieFed <rimu@chorebuster.net>',
|
||||||
recipients=[user.email],
|
recipients=[user.email],
|
||||||
text_body=render_template('email/verification.txt', user=user),
|
text_body=render_template('email/verification.txt', user=user),
|
||||||
html_body=render_template('email/verification.html', user=user))
|
html_body=render_template('email/verification.html', user=user))
|
|
@ -45,7 +45,7 @@ class CreatePostForm(FlaskForm):
|
||||||
nsfw = BooleanField(_l('NSFW'))
|
nsfw = BooleanField(_l('NSFW'))
|
||||||
nsfl = BooleanField(_l('NSFL'))
|
nsfl = BooleanField(_l('NSFL'))
|
||||||
notify_author = BooleanField(_l('Notify about replies'))
|
notify_author = BooleanField(_l('Notify about replies'))
|
||||||
submit = SubmitField(_l('Post'))
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
def validate(self, extra_validators=None) -> bool:
|
def validate(self, extra_validators=None) -> bool:
|
||||||
if not super().validate():
|
if not super().validate():
|
||||||
|
|
|
@ -4,17 +4,19 @@ from flask_babel import _
|
||||||
from pillow_heif import register_heif_opener
|
from pillow_heif import register_heif_opener
|
||||||
from sqlalchemy import or_, desc
|
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.signature import RsaKeys, HttpSignature
|
||||||
|
from app.activitypub.util import default_context
|
||||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm
|
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm
|
||||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
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
|
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, \
|
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||||
File, PostVote
|
File, PostVote
|
||||||
from app.community import bp
|
from app.community import bp
|
||||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
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
|
import os
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -70,7 +72,7 @@ def add_remote():
|
||||||
|
|
||||||
return render_template('community/add_remote.html',
|
return render_template('community/add_remote.html',
|
||||||
title=_('Add remote community'), form=form, new_community=new_community,
|
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.
|
# @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,
|
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,
|
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'])
|
@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()
|
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||||
|
|
||||||
if community is not None:
|
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:
|
if remote:
|
||||||
# send ActivityPub message to remote community, asking to follow. Accept message will be sent to our shared inbox
|
# 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)
|
join_request = CommunityJoinRequest(user_id=current_user.id, community_id=community.id)
|
||||||
|
@ -161,12 +164,46 @@ def unsubscribe(actor):
|
||||||
community = actor_to_community(actor)
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
if community is not None:
|
if community is not None:
|
||||||
subscription = current_user.subscribed(community)
|
subscription = community_membership(current_user, community)
|
||||||
if subscription:
|
if subscription:
|
||||||
if subscription != SUBSCRIPTION_OWNER:
|
if subscription != SUBSCRIPTION_OWNER:
|
||||||
db.session.query(CommunityMember).filter_by(user_id=current_user.id, community_id=community.id).delete()
|
proceed = True
|
||||||
db.session.commit()
|
# Undo the Follow
|
||||||
flash('You are unsubscribed from ' + community.title)
|
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:
|
else:
|
||||||
# todo: community deletion
|
# todo: community deletion
|
||||||
flash('You need to make someone else the owner before unsubscribing.', 'warning')
|
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_profile_id=community_json['id'],
|
||||||
ap_followers_url=community_json['followers'],
|
ap_followers_url=community_json['followers'],
|
||||||
ap_inbox_url=community_json['endpoints']['sharedInbox'],
|
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_fetched_at=datetime.utcnow(),
|
||||||
ap_domain=server,
|
ap_domain=server,
|
||||||
public_key=community_json['publicKey']['publicKeyPem'],
|
public_key=community_json['publicKey']['publicKeyPem'],
|
||||||
|
|
|
@ -13,6 +13,7 @@ SUBSCRIPTION_OWNER = 3
|
||||||
SUBSCRIPTION_MODERATOR = 2
|
SUBSCRIPTION_MODERATOR = 2
|
||||||
SUBSCRIPTION_MEMBER = 1
|
SUBSCRIPTION_MEMBER = 1
|
||||||
SUBSCRIPTION_NONMEMBER = 0
|
SUBSCRIPTION_NONMEMBER = 0
|
||||||
SUBSCRIPTION_BANNED = -1
|
SUBSCRIPTION_PENDING = -1
|
||||||
|
SUBSCRIPTION_BANNED = -2
|
||||||
|
|
||||||
THREAD_CUTOFF_DEPTH = 4
|
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 app.main import bp
|
||||||
from flask import g, session, flash, request
|
from flask import g, session, flash, request
|
||||||
from flask_moment import moment
|
from flask_moment import moment
|
||||||
|
@ -8,7 +9,6 @@ from flask_babel import _, get_locale
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy_searchable import search
|
from sqlalchemy_searchable import search
|
||||||
from app.utils import render_template, get_setting, gibberish
|
from app.utils import render_template, get_setting, gibberish
|
||||||
|
|
||||||
from app.models import Community, CommunityMember
|
from app.models import Community, CommunityMember
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,21 +29,29 @@ def list_communities():
|
||||||
query = search(select(Community), search_param, sort=True)
|
query = search(select(Community), search_param, sort=True)
|
||||||
communities = db.session.scalars(query).all()
|
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'])
|
@bp.route('/communities/local', methods=['GET'])
|
||||||
def list_local_communities():
|
def list_local_communities():
|
||||||
verification_warning()
|
verification_warning()
|
||||||
communities = Community.query.filter_by(ap_id=None, banned=False).all()
|
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'])
|
@bp.route('/communities/subscribed', methods=['GET'])
|
||||||
def list_subscribed_communities():
|
def list_subscribed_communities():
|
||||||
verification_warning()
|
verification_warning()
|
||||||
communities = Community.query.filter_by(banned=False).join(CommunityMember).filter(CommunityMember.user_id == current_user.id).all()
|
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():
|
def verification_warning():
|
||||||
|
|
|
@ -17,7 +17,7 @@ import jwt
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER, \
|
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER, \
|
||||||
SUBSCRIPTION_BANNED
|
SUBSCRIPTION_BANNED, SUBSCRIPTION_PENDING
|
||||||
|
|
||||||
|
|
||||||
class FullTextSearchQuery(BaseQuery, SearchQueryMixin):
|
class FullTextSearchQuery(BaseQuery, SearchQueryMixin):
|
||||||
|
@ -87,6 +87,7 @@ class Community(db.Model):
|
||||||
ap_fetched_at = db.Column(db.DateTime)
|
ap_fetched_at = db.Column(db.DateTime)
|
||||||
ap_deleted_at = db.Column(db.DateTime)
|
ap_deleted_at = db.Column(db.DateTime)
|
||||||
ap_inbox_url = db.Column(db.String(255))
|
ap_inbox_url = db.Column(db.String(255))
|
||||||
|
ap_moderators_url = db.Column(db.String(255))
|
||||||
ap_domain = db.Column(db.String(255))
|
ap_domain = db.Column(db.String(255))
|
||||||
|
|
||||||
banned = db.Column(db.Boolean, default=False)
|
banned = db.Column(db.Boolean, default=False)
|
||||||
|
@ -276,11 +277,10 @@ class User(UserMixin, db.Model):
|
||||||
return True
|
return True
|
||||||
return self.expires < datetime(2019, 9, 1)
|
return self.expires < datetime(2019, 9, 1)
|
||||||
|
|
||||||
@cache.memoize(timeout=50)
|
def subscribed(self, community_id: int) -> int:
|
||||||
def subscribed(self, community: Community) -> int:
|
if community_id is None:
|
||||||
if community is None:
|
|
||||||
return False
|
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:
|
||||||
if subscription.is_banned:
|
if subscription.is_banned:
|
||||||
return SUBSCRIPTION_BANNED
|
return SUBSCRIPTION_BANNED
|
||||||
|
@ -291,7 +291,11 @@ class User(UserMixin, db.Model):
|
||||||
else:
|
else:
|
||||||
return SUBSCRIPTION_MEMBER
|
return SUBSCRIPTION_MEMBER
|
||||||
else:
|
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]:
|
def communities(self) -> List[Community]:
|
||||||
return Community.query.filter(Community.banned == False).\
|
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")
|
image = db.relationship(File, lazy='joined', foreign_keys=[image_id], cascade="all, delete")
|
||||||
domain = db.relationship('Domain', lazy='joined', foreign_keys=[domain_id])
|
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
|
@classmethod
|
||||||
def get_by_ap_id(cls, ap_id):
|
def get_by_ap_id(cls, ap_id):
|
||||||
|
|
|
@ -26,8 +26,8 @@
|
||||||
{{ render_field(form.discussion_body) }}
|
{{ render_field(form.discussion_body) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
|
<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_url) }}
|
||||||
|
{{ render_field(form.link_title) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
||||||
{{ render_field(form.image_title) }}
|
{{ render_field(form.image_title) }}
|
||||||
|
@ -39,6 +39,9 @@
|
||||||
</div>
|
</div>
|
||||||
{{ render_field(form.type) }}
|
{{ render_field(form.type) }}
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ render_field(form.notify_author) }}
|
||||||
|
</div>
|
||||||
<div class="col-md-1">
|
<div class="col-md-1">
|
||||||
{{ render_field(form.nsfw) }}
|
{{ render_field(form.nsfw) }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,7 +52,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ render_field(form.notify_author) }}
|
|
||||||
{{ render_field(form.submit) }}
|
{{ render_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -49,8 +49,10 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<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>
|
<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 %}
|
{% else %}
|
||||||
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||||
{% endif %}
|
{% 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>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'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>
|
<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>
|
<ol>
|
||||||
<li>Helpful resources goes here</li>
|
<li>Helpful resources goes here</li>
|
||||||
</ol>
|
</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>Warm regards,</p>
|
||||||
<p>Rimu Atkinson<br />
|
<p>Rimu Atkinson<br />
|
||||||
Founder</p>
|
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
|
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,
|
Warm regards,
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,10 @@
|
||||||
<td>{{ community.post_reply_count }}</td>
|
<td>{{ community.post_reply_count }}</td>
|
||||||
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
|
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
|
||||||
<td>{% if current_user.is_authenticated %}
|
<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>
|
<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 %}
|
{% else %}
|
||||||
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/subscribe">Subscribe</a>
|
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/subscribe">Subscribe</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<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>
|
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
<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="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<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>
|
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
<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="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<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>
|
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||||
|
|
|
@ -50,8 +50,8 @@
|
||||||
{{ render_field(form.discussion_body) }}
|
{{ render_field(form.discussion_body) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
|
<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_url) }}
|
||||||
|
{{ render_field(form.link_title) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
||||||
{{ render_field(form.image_title) }}
|
{{ render_field(form.image_title) }}
|
||||||
|
@ -63,17 +63,21 @@
|
||||||
</div>
|
</div>
|
||||||
{{ render_field(form.type) }}
|
{{ render_field(form.type) }}
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ render_field(form.notify_author) }}
|
||||||
|
</div>
|
||||||
<div class="col-md-1">
|
<div class="col-md-1">
|
||||||
{{ render_field(form.nsfw) }}
|
{{ render_field(form.nsfw) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1">
|
<div class="col-md-1">
|
||||||
{{ render_field(form.nsfl) }}
|
{{ render_field(form.nsfl) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ render_field(form.notify_author) }}
|
|
||||||
{{ render_field(form.submit) }}
|
{{ render_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
11
app/utils.py
11
app/utils.py
|
@ -14,7 +14,7 @@ from flask_login import current_user
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from app import db, cache
|
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
|
# 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
|
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():
|
def retrieve_block_list():
|
||||||
try:
|
try:
|
||||||
response = requests.get('https://github.com/rimu/no-qanon/blob/master/domains.txt', timeout=1)
|
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
|
import os, click
|
||||||
from flask import session, g, json
|
from flask import session, g, json
|
||||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
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()
|
app = create_app()
|
||||||
cli.register(app)
|
cli.register(app)
|
||||||
|
@ -29,6 +29,7 @@ with app.app_context():
|
||||||
app.jinja_env.globals['len'] = len
|
app.jinja_env.globals['len'] = len
|
||||||
app.jinja_env.globals['digits'] = digits
|
app.jinja_env.globals['digits'] = digits
|
||||||
app.jinja_env.globals['str'] = str
|
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['json_loads'] = json.loads
|
||||||
app.jinja_env.globals['user_access'] = user_access
|
app.jinja_env.globals['user_access'] = user_access
|
||||||
app.jinja_env.filters['shorten'] = shorten_string
|
app.jinja_env.filters['shorten'] = shorten_string
|
||||||
|
|
Loading…
Reference in a new issue