Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Martynas Sklizmantas 2024-03-23 17:21:27 +01:00
commit 0ecdcd4d1e
31 changed files with 430 additions and 126 deletions

View file

@ -18,7 +18,11 @@
## Setup Database
#### Install postgresql 16
#### Install postgresql
PieFed should work on version 13.x or newer. If you have errors running `flask init-db`, check your postrgesql version.
##### Install postgresql 16:
For installation environments that use 'apt' as a package manager:
@ -351,6 +355,16 @@ at ~/.aws/credentials or environment variables. Details at https://boto3.amazona
In your .env you need to set the AWS region you're using for SES. Something like AWS_REGION = 'ap-southeast-2'.
#### CDN
A CDN like Cloudflare is recommended for instances with more than a handful of users. [Recommended caching settings](https://join.piefed.social/2024/02/20/how-much-difference-does-a-cdn-make-to-a-fediverse-instance/).
PieFed has the capability to automatically remove file copies from the Cloudflare cache whenever
those files are deleted from the server. To enable this, set these variables in your .env file:
- CLOUDFLARE_API_TOKEN - go to https://dash.cloudflare.com/profile/api-tokens and create a "Zone.Cache Purge" token.
- CLOUDFLARE_ZONE_ID - this can be found in the right hand column of your Cloudflare dashboard in the API section.
#### SMTP
To use SMTP you need to set all the MAIL_* environment variables in you .env file. See env.sample for a list of them.

View file

@ -195,7 +195,7 @@ def user_profile(actor):
if is_activitypub_request():
server = current_app.config['SERVER_NAME']
actor_data = { "@context": default_context(),
"type": "Person",
"type": "Person" if not user.bot else "Service",
"id": f"https://{server}/u/{actor}",
"preferredUsername": actor,
"name": user.title if user.title else user.user_name,
@ -203,7 +203,7 @@ def user_profile(actor):
"outbox": f"https://{server}/u/{actor}/outbox",
"discoverable": user.searchable,
"indexable": user.indexable,
"manuallyApprovesFollowers": user.ap_manually_approves_followers,
"manuallyApprovesFollowers": False if not user.ap_manually_approves_followers else user.ap_manually_approves_followers,
"publicKey": {
"id": f"https://{server}/u/{actor}#main-key",
"owner": f"https://{server}/u/{actor}",
@ -666,24 +666,48 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
target_ap_id = request_json['object']['object']['object'] # object object object!
post = undo_vote(activity_log, comment, post, target_ap_id, user)
activity_log.result = 'success'
elif request_json['object']['type'] == 'Add':
elif request_json['object']['type'] == 'Add' and 'target' in request_json['object']:
activity_log.activity_type = request_json['object']['type']
featured_url = Community.query.filter(Community.ap_public_url == request_json['actor']).first().ap_featured_url
if featured_url:
if 'target' in request_json['object'] and featured_url == request_json['object']['target']:
post = Post.query.filter(Post.ap_id == request_json['object']['object']).first()
target = request_json['object']['target']
community = Community.query.filter_by(ap_public_url=request_json['actor']).first()
if community:
featured_url = community.ap_featured_url
moderators_url = community.ap_moderators_url
if target == featured_url:
post = Post.query.filter_by(ap_id=request_json['object']['object']).first()
if post:
post.sticky = True
activity_log.result = 'success'
elif request_json['object']['type'] == 'Remove':
if target == moderators_url:
user = find_actor_or_create(request_json['object']['object'])
if user:
existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first()
if existing_membership:
existing_membership.is_moderator = True
else:
new_membership = CommunityMember(community_id=community.id, user_id=user.id, is_moderator=True)
db.session.add(new_membership)
db.session.commit()
activity_log.result = 'success'
elif request_json['object']['type'] == 'Remove' and 'target' in request_json['object']:
activity_log.activity_type = request_json['object']['type']
featured_url = Community.query.filter(Community.ap_public_url == request_json['actor']).first().ap_featured_url
if featured_url:
if 'target' in request_json['object'] and featured_url == request_json['object']['target']:
post = Post.query.filter(Post.ap_id == request_json['object']['object']).first()
target = request_json['object']['target']
community = Community.query.filter_by(ap_public_url=request_json['actor']).first()
if community:
featured_url = community.ap_featured_url
moderators_url = community.ap_moderators_url
if target == featured_url:
post = Post.query.filter_by(ap_id=request_json['object']['object']).first()
if post:
post.sticky = False
activity_log.result = 'success'
if target == moderators_url:
user = find_actor_or_create(request_json['object']['object'], create_if_not_found=False)
if user:
existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first()
if existing_membership:
existing_membership.is_moderator = False
activity_log.result = 'success'
else:
activity_log.exception_message = 'Invalid type for Announce'

View file

@ -24,7 +24,8 @@ import pytesseract
from app.utils import get_request, allowlist_html, html_to_markdown, get_setting, ap_datetime, markdown_to_html, \
is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request, post_ranking, \
shorten_string, reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, remove_tracking_from_link
shorten_string, reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, remove_tracking_from_link, \
blocked_phrases
def public_key():
@ -488,7 +489,7 @@ def refresh_community_profile_task(community_id):
def actor_json_to_model(activity_json, address, server):
if activity_json['type'] == 'Person':
if activity_json['type'] == 'Person' or activity_json['type'] == 'Service':
try:
user = User(user_name=activity_json['preferredUsername'],
title=activity_json['name'] if 'name' in activity_json else None,
@ -508,6 +509,7 @@ def actor_json_to_model(activity_json, address, server):
ap_fetched_at=utcnow(),
ap_domain=server,
public_key=activity_json['publicKey']['publicKeyPem'],
bot=True if activity_json['type'] == 'Service' else False,
instance_id=find_instance_id(server)
# language=community_json['language'][0]['identifier'] # todo: language
)
@ -1158,6 +1160,7 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
root_id=root_id,
nsfw=community.nsfw,
nsfl=community.nsfl,
from_bot=user.bot,
up_votes=1,
depth=depth,
score=instance_weight(user.ap_domain),
@ -1175,6 +1178,11 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
post_reply.body_html = allowlist_html(request_json['object']['content'])
post_reply.body = html_to_markdown(post_reply.body_html)
if post_id is not None:
# Discard post_reply if it contains certain phrases. Good for stopping spam floods.
if post_reply.body:
for blocked_phrase in blocked_phrases():
if blocked_phrase in post_reply.body:
return None
post = Post.query.get(post_id)
if post.comments_enabled:
anchor = None
@ -1256,6 +1264,7 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
ap_announce_id=announce_id,
type=constants.POST_TYPE_ARTICLE,
up_votes=1,
from_bot=user.bot,
score=instance_weight(user.ap_domain),
instance_id=user.instance_id,
indexable=user.indexable
@ -1267,6 +1276,15 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
elif 'content' in request_json['object'] and request_json['object']['content'] is not None: # Kbin
post.body_html = allowlist_html(request_json['object']['content'])
post.body = html_to_markdown(post.body_html)
# Discard post if it contains certain phrases. Good for stopping spam floods.
blocked_phrases_list = blocked_phrases()
for blocked_phrase in blocked_phrases_list:
if blocked_phrase in post.title:
return None
if post.body:
for blocked_phrase in blocked_phrases_list:
if blocked_phrase in post.body:
return None
if 'attachment' in request_json['object'] and len(request_json['object']['attachment']) > 0 and \
'type' in request_json['object']['attachment'][0]:
if request_json['object']['attachment'][0]['type'] == 'Link':

View file

@ -31,6 +31,7 @@ class SiteMiscForm(FlaskForm):
types = [('Open', _l('Open')), ('RequireApplication', _l('Require application')), ('Closed', _l('Closed'))]
registration_mode = SelectField(_l('Registration mode'), choices=types, default=1, coerce=str)
application_question = TextAreaField(_l('Question to ask people applying for an account'))
auto_decline_referrers = TextAreaField(_l('Block registrations from these referrers (one per line)'))
log_activitypub_json = BooleanField(_l('Log ActivityPub JSON for debugging'))
default_theme = SelectField(_l('Default theme'), coerce=str)
submit = SubmitField(_l('Save'))
@ -41,6 +42,7 @@ class FederationForm(FlaskForm):
allowlist = TextAreaField(_l('Allow federation with these instances'))
use_blocklist = BooleanField(_l('Blocklist instead of allowlist'))
blocklist = TextAreaField(_l('Deny federation with these instances'))
blocked_phrases = TextAreaField(_l('Discard all posts and comments with these phrases (one per line)'))
submit = SubmitField(_l('Save'))
@ -165,26 +167,16 @@ class AddUserForm(FlaskForm):
class EditUserForm(FlaskForm):
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
email = StringField(_l('Email address'), validators=[Optional(), Length(max=255)])
matrix_user_id = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)])
profile_file = FileField(_l('Avatar image'))
banner_file = FileField(_l('Top banner image'))
bot = BooleanField(_l('This profile is a bot'))
verified = BooleanField(_l('Email address is verified'))
banned = BooleanField(_l('Banned'))
newsletter = BooleanField(_l('Subscribe to email newsletter'))
ignore_bots = BooleanField(_l('Hide posts by bots'))
nsfw = BooleanField(_l('Show NSFW posts'))
nsfl = BooleanField(_l('Show NSFL posts'))
searchable = BooleanField(_l('Show profile in user list'))
indexable = BooleanField(_l('Allow search engines to index this profile'))
manually_approves_followers = BooleanField(_l('Manually approve followers'))
role_options = [(2, _l('User')),
(3, _l('Staff')),
(4, _l('Admin')),
]
role = SelectField(_l('Role'), choices=role_options, default=2, coerce=int)
remove_avatar = BooleanField(_l('Remove avatar'))
remove_banner = BooleanField(_l('Remove banner'))
submit = SubmitField(_l('Save'))

View file

@ -6,10 +6,10 @@ from flask_login import login_required, current_user
from flask_babel import _
from sqlalchemy import text, desc
from app import db, celery
from app import db, celery, cache
from app.activitypub.routes import process_inbox_request, process_delete_request
from app.activitypub.signature import post_request
from app.activitypub.util import default_context
from app.activitypub.util import default_context, instance_allowed, instance_blocked
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \
EditTopicForm, SendNewsletterForm, AddUserForm
from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community, send_newsletter, \
@ -18,7 +18,7 @@ from app.community.util import save_icon_file, save_banner_file
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
User, Instance, File, Report, Topic, UserRegistration, Role, Post
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \
moderating_communities, joined_communities, finalize_user_setup, theme_list
moderating_communities, joined_communities, finalize_user_setup, theme_list, blocked_phrases, blocked_referrers
from app.admin import bp
@ -80,12 +80,14 @@ def admin_misc():
site.reports_email_admins = form.reports_email_admins.data
site.registration_mode = form.registration_mode.data
site.application_question = form.application_question.data
site.auto_decline_referrers = form.auto_decline_referrers.data
site.log_activitypub_json = form.log_activitypub_json.data
site.updated = utcnow()
site.default_theme = form.default_theme.data
if site.id is None:
db.session.add(site)
db.session.commit()
cache.delete_memoized(blocked_referrers)
flash('Settings saved.')
elif request.method == 'GET':
form.enable_downvotes.data = site.enable_downvotes
@ -97,6 +99,7 @@ def admin_misc():
form.reports_email_admins.data = site.reports_email_admins
form.registration_mode.data = site.registration_mode
form.application_question.data = site.application_question
form.auto_decline_referrers.data = site.auto_decline_referrers
form.log_activitypub_json.data = site.log_activitypub_json
form.default_theme.data = site.default_theme if site.default_theme is not None else ''
return render_template('admin/misc.html', title=_('Misc settings'), form=form,
@ -123,13 +126,18 @@ def admin_federation():
for allow in form.allowlist.data.split('\n'):
if allow.strip():
db.session.add(AllowedInstances(domain=allow.strip()))
cache.delete_memoized(instance_allowed, allow.strip())
if form.use_blocklist.data:
set_setting('use_allowlist', False)
db.session.execute(text('DELETE FROM banned_instances'))
for banned in form.blocklist.data.split('\n'):
if banned.strip():
db.session.add(BannedInstances(domain=banned.strip()))
cache.delete_memoized(instance_blocked, banned.strip())
site.blocked_phrases = form.blocked_phrases.data
cache.delete_memoized(blocked_phrases)
db.session.commit()
flash(_('Admin settings saved'))
elif request.method == 'GET':
@ -139,6 +147,7 @@ def admin_federation():
form.blocklist.data = '\n'.join([instance.domain for instance in instances])
instances = AllowedInstances.query.all()
form.allowlist.data = '\n'.join([instance.domain for instance in instances])
form.blocked_phrases.data = site.blocked_phrases
return render_template('admin/federation.html', title=_('Federation settings'), form=form,
moderating_communities=moderating_communities(current_user.get_id()),
@ -526,46 +535,20 @@ def admin_user_edit(user_id):
form = EditUserForm()
user = User.query.get_or_404(user_id)
if form.validate_on_submit():
user.about = form.about.data
user.email = form.email.data
user.about_html = markdown_to_html(form.about.data)
user.matrix_user_id = form.matrix_user_id.data
user.bot = form.bot.data
user.verified = form.verified.data
user.banned = form.banned.data
profile_file = request.files['profile_file']
if profile_file and profile_file.filename != '':
# remove old avatar
if user.avatar_id:
file = File.query.get(user.avatar_id)
file.delete_from_disk()
user.avatar_id = None
db.session.delete(file)
if form.remove_avatar.data and user.avatar_id:
file = File.query.get(user.avatar_id)
file.delete_from_disk()
user.avatar_id = None
db.session.delete(file)
# add new avatar
file = save_icon_file(profile_file, 'users')
if file:
user.avatar = file
banner_file = request.files['banner_file']
if banner_file and banner_file.filename != '':
# remove old cover
if user.cover_id:
file = File.query.get(user.cover_id)
file.delete_from_disk()
user.cover_id = None
db.session.delete(file)
# add new cover
file = save_banner_file(banner_file, 'users')
if file:
user.cover = file
user.newsletter = form.newsletter.data
user.ignore_bots = form.ignore_bots.data
user.show_nsfw = form.nsfw.data
user.show_nsfl = form.nsfl.data
user.searchable = form.searchable.data
user.indexable = form.indexable.data
user.ap_manually_approves_followers = form.manually_approves_followers.data
if form.remove_banner.data and user.cover_id:
file = File.query.get(user.cover_id)
file.delete_from_disk()
user.cover_id = None
db.session.delete(file)
# Update user roles. The UI only lets the user choose 1 role but the DB structure allows for multiple roles per user.
db.session.execute(text('DELETE FROM user_role WHERE user_id = :user_id'), {'user_id': user.id})
@ -580,19 +563,9 @@ def admin_user_edit(user_id):
else:
if not user.is_local():
flash(_('This is a remote user - most settings here will be regularly overwritten with data from the original server.'), 'warning')
form.about.data = user.about
form.email.data = user.email
form.matrix_user_id.data = user.matrix_user_id
form.newsletter.data = user.newsletter
form.bot.data = user.bot
form.verified.data = user.verified
form.banned.data = user.banned
form.ignore_bots.data = user.ignore_bots
form.nsfw.data = user.show_nsfw
form.nsfl.data = user.show_nsfl
form.searchable.data = user.searchable
form.indexable.data = user.indexable
form.manually_approves_followers.data = user.ap_manually_approves_followers
if user.roles and user.roles.count() > 0:
form.role.data = user.roles[0].id

View file

@ -12,7 +12,7 @@ from app.auth.util import random_token, normalize_utf
from app.email import send_verification_email, send_password_reset_email
from app.models import User, utcnow, IpBan, UserRegistration, Notification, Site
from app.utils import render_template, ip_address, user_ip_banned, user_cookie_banned, banned_ip_addresses, \
finalize_user_setup
finalize_user_setup, blocked_referrers
@bp.route('/login', methods=['GET', 'POST'])
@ -98,6 +98,11 @@ def register():
if form.user_name.data in disallowed_usernames:
flash(_('Sorry, you cannot use that user name'), 'error')
else:
for referrer in blocked_referrers():
if referrer in session.get('Referer'):
resp = make_response(redirect(url_for('auth.please_wait')))
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
return resp
verification_token = random_token(16)
form.user_name.data = form.user_name.data.strip()
before_normalize = form.user_name.data

View file

@ -68,7 +68,7 @@ class SearchRemoteCommunity(FlaskForm):
class BanUserCommunityForm(FlaskForm):
reason = StringField(_l('Reason'), render_kw={'autofocus': True}, validators=[DataRequired()])
ban_until = DateField(_l('Ban until'))
ban_until = DateField(_l('Ban until'), validators=[Optional()])
delete_posts = BooleanField(_l('Also delete all their posts'))
delete_post_replies = BooleanField(_l('Also delete all their comments'))
submit = SubmitField(_l('Ban'))

View file

@ -27,7 +27,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
shorten_string, gibberish, community_membership, ap_datetime, \
request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \
joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \
community_moderators
community_moderators, communities_banned_from
from feedgen.feed import FeedGenerator
from datetime import timezone, timedelta
@ -793,6 +793,9 @@ def community_ban_user(community_id: int, user_id: int):
form = BanUserCommunityForm()
if form.validate_on_submit():
# Both CommunityBan and CommunityMember need to be updated. CommunityBan is under the control of moderators while
# CommunityMember can be cleared by the user by leaving the group and rejoining. CommunityMember.is_banned stops
# posts from the community from showing up in the banned person's home feed.
if not existing:
new_ban = CommunityBan(community_id=community_id, user_id=user.id, banned_by=current_user.id,
reason=form.reason.data)
@ -800,7 +803,13 @@ def community_ban_user(community_id: int, user_id: int):
new_ban.ban_until = form.ban_until.data
db.session.add(new_ban)
db.session.commit()
flash(_('%(name)s has been banned.', name=user.display_name()))
community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first()
if community_membership_record:
community_membership_record.is_banned = True
db.session.commit()
flash(_('%(name)s has been banned.', name=user.display_name()))
if form.delete_posts.data:
posts = Post.query.filter(Post.user_id == user.id, Post.community_id == community.id).all()
@ -819,8 +828,11 @@ def community_ban_user(community_id: int, user_id: int):
# notify banned person
if user.is_local():
cache.delete_memoized(communities_banned_from, user.id)
cache.delete_memoized(joined_communities, user.id)
cache.delete_memoized(moderating_communities, user.id)
notify = Notification(title=shorten_string('You have been banned from ' + community.title),
url=f'/', user_id=user.id,
url=f'/notifications', user_id=user.id,
author_id=1)
db.session.add(notify)
user.unread_notifications += 1
@ -839,6 +851,42 @@ def community_ban_user(community_id: int, user_id: int):
)
@bp.route('/community/<int:community_id>/<int:user_id>/unban_user_community', methods=['GET', 'POST'])
@login_required
def community_unban_user(community_id: int, user_id: int):
community = Community.query.get_or_404(community_id)
user = User.query.get_or_404(user_id)
existing_ban = CommunityBan.query.filter_by(community_id=community.id, user_id=user.id).first()
if existing_ban:
db.session.delete(existing_ban)
db.session.commit()
community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first()
if community_membership_record:
community_membership_record.is_banned = False
db.session.commit()
flash(_('%(name)s has been unbanned.', name=user.display_name()))
# todo: federate ban to post author instance
# notify banned person
if user.is_local():
cache.delete_memoized(communities_banned_from, user.id)
cache.delete_memoized(joined_communities, user.id)
cache.delete_memoized(moderating_communities, user.id)
notify = Notification(title=shorten_string('You have been un-banned from ' + community.title),
url=f'/notifications', user_id=user.id,
author_id=1)
db.session.add(notify)
user.unread_notifications += 1
db.session.commit()
else:
...
# todo: send chatmessage to remote user and federate it
return redirect(url_for('community.community_moderate_banned', actor=community.link()))
@bp.route('/<int:community_id>/notification', methods=['GET', 'POST'])
@login_required

View file

@ -16,7 +16,7 @@ from app.models import Community, File, BannedInstances, PostReply, PostVote, Po
Instance, Notification, User, ActivityPubLog
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
remove_tracking_from_link, ap_datetime, instance_banned
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases
from sqlalchemy import func, desc
import os
@ -122,7 +122,7 @@ def retrieve_mods_and_backfill(community_id: int):
c.last_active = Post.query.filter(Post.community_id == community_id).order_by(desc(Post.posted_at)).first().posted_at
db.session.commit()
if community.ap_featured_url:
featured_request = get_request(community.ap_featured_url, headers={'Accept': 'application/activityjson'})
featured_request = get_request(community.ap_featured_url, headers={'Accept': 'application/activity+json'})
if featured_request.status_code == 200:
featured_data = featured_request.json()
featured_request.close()
@ -299,6 +299,19 @@ def save_post(form, post: Post):
if current_user.reputation < -100:
post.score = -1
post.ranking = post_ranking(post.score, utcnow())
# Filter by phrase
blocked_phrases_list = blocked_phrases()
for blocked_phrase in blocked_phrases_list:
if blocked_phrase in post.title:
abort(401)
return
if post.body:
for blocked_phrase in blocked_phrases_list:
if blocked_phrase in post.body:
abort(401)
return
db.session.add(post)
g.site.last_active = utcnow()

View file

@ -5,6 +5,7 @@ from random import randint
import flask
import markdown2
import requests
from sqlalchemy.sql.operators import or_, and_
from app import db, cache
@ -24,7 +25,7 @@ from sqlalchemy_searchable import search
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \
joined_communities, moderating_communities, parse_page, theme_list, get_request, markdown_to_html, allowlist_html, \
blocked_instances
blocked_instances, communities_banned_from
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \
InstanceRole, Notification
from PIL import Image
@ -131,7 +132,13 @@ def home_page(type, sort):
next_url = url_for('main.all_posts', page=posts.next_num, sort=sort) if posts.has_next else None
prev_url = url_for('main.all_posts', page=posts.prev_num, sort=sort) if posts.has_prev and page != 1 else None
active_communities = Community.query.filter_by(banned=False).order_by(desc(Community.last_active)).limit(5).all()
# Active Communities
active_communities = Community.query.filter_by(banned=False)
if current_user.is_authenticated: # do not show communities current user is banned from
banned_from = communities_banned_from(current_user.id)
if banned_from:
active_communities = active_communities.filter(Community.id.not_in(banned_from))
active_communities = active_communities.order_by(desc(Community.last_active)).limit(5).all()
return render_template('index.html', posts=posts, active_communities=active_communities, show_post_community=True,
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
@ -178,6 +185,11 @@ def list_communities():
if topic_id != 0:
communities = communities.filter_by(topic_id=topic_id)
if current_user.is_authenticated:
banned_from = communities_banned_from(current_user.id)
if banned_from:
communities = communities.filter(Community.id.not_in(banned_from))
return render_template('list_communities.html', communities=communities.order_by(sort_by).all(), search=search_param, title=_('Communities'),
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
@ -265,8 +277,7 @@ def list_files(directory):
@bp.route('/test')
def test():
x = find_actor_or_create('artporn@lemm.ee')
return 'ok'
return ''
users_to_notify = User.query.join(Notification, User.id == Notification.user_id).filter(
User.ap_id == None,

View file

@ -1,7 +1,8 @@
from datetime import datetime, timedelta, date, timezone
from time import time
from typing import List
from typing import List, Union
import requests
from flask import current_app, escape, url_for, render_template_string
from flask_login import UserMixin, current_user
from sqlalchemy import or_, text
@ -11,7 +12,7 @@ from sqlalchemy.orm import backref
from sqlalchemy_utils.types import TSVectorType # https://sqlalchemy-searchable.readthedocs.io/en/latest/installation.html
from flask_sqlalchemy import BaseQuery
from sqlalchemy_searchable import SearchQueryMixin
from app import db, login, cache
from app import db, login, cache, celery
import jwt
import os
@ -203,12 +204,20 @@ class File(db.Model):
return f"https://{current_app.config['SERVER_NAME']}/{thumbnail_path}"
def delete_from_disk(self):
purge_from_cache = []
if self.file_path and os.path.isfile(self.file_path):
os.unlink(self.file_path)
purge_from_cache.append(self.file_path.replace('app/', f"https://{current_app.config['SERVER_NAME']}/"))
if self.thumbnail_path and os.path.isfile(self.thumbnail_path):
os.unlink(self.thumbnail_path)
purge_from_cache.append(self.thumbnail_path.replace('app/', f"https://{current_app.config['SERVER_NAME']}/"))
if self.source_url and not self.source_url.startswith('http') and os.path.isfile(self.source_url):
os.unlink(self.source_url)
purge_from_cache.append(self.source_url.replace('app/', f"https://{current_app.config['SERVER_NAME']}/"))
if purge_from_cache:
flush_cdn_cache(purge_from_cache)
def filesize(self):
size = 0
@ -219,6 +228,50 @@ class File(db.Model):
return size
def flush_cdn_cache(url: Union[str, List[str]]):
zone_id = current_app.config['CLOUDFLARE_ZONE_ID']
token = current_app.config['CLOUDFLARE_API_TOKEN']
if zone_id and token:
if current_app.debug:
flush_cdn_cache_task(url)
else:
flush_cdn_cache_task.delay(url)
@celery.task
def flush_cdn_cache_task(to_purge: Union[str, List[str]]):
zone_id = current_app.config['CLOUDFLARE_ZONE_ID']
token = current_app.config['CLOUDFLARE_API_TOKEN']
headers = {
'Authorization': f"Bearer {token}",
'Content-Type': 'application/json'
}
# url can be a string or a list of strings
body = ''
if isinstance(to_purge, str) and to_purge == 'all':
body = {
'purge_everything': True
}
else:
if isinstance(to_purge, str):
body = {
'files': [to_purge]
}
elif isinstance(to_purge, list):
body = {
'files': to_purge
}
if body:
response = requests.request(
'POST',
f'https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache',
headers=headers,
json=body,
timeout=5,
)
class Topic(db.Model):
id = db.Column(db.Integer, primary_key=True)
machine_name = db.Column(db.String(50), index=True)
@ -488,7 +541,7 @@ class User(UserMixin, db.Model):
ap_fetched_at = db.Column(db.DateTime)
ap_followers_url = db.Column(db.String(255))
ap_preferred_username = db.Column(db.String(255))
ap_manually_approves_followers = db.Column(db.Boolean)
ap_manually_approves_followers = db.Column(db.Boolean, default=False)
ap_deleted_at = db.Column(db.DateTime)
ap_inbox_url = db.Column(db.String(255))
ap_domain = db.Column(db.String(255))
@ -579,8 +632,8 @@ class User(UserMixin, db.Model):
def num_content(self):
content = 0
content += db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE user_id = ' + str(self.id))).scalar()
content += db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE user_id = ' + str(self.id))).scalar()
content += db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE user_id = :user_id'), {'user_id': self.id}).scalar()
content += db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE user_id = :user_id'), {'user_id': self.id}).scalar()
return content
def is_local(self):
@ -1170,7 +1223,8 @@ class Site(db.Model):
allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list
allowlist = db.Column(db.Text, default='')
blocklist = db.Column(db.Text, default='')
auto_decline_referrers = db.Column(db.Text, default='rdrama.net')
blocked_phrases = db.Column(db.Text, default='') # discard incoming content with these phrases
auto_decline_referrers = db.Column(db.Text, default='rdrama.net\nahrefs.com') # automatically decline registration requests if the referrer is one of these
created_at = db.Column(db.DateTime, default=utcnow)
updated = db.Column(db.DateTime, default=utcnow)
last_active = db.Column(db.DateTime, default=utcnow)

View file

@ -18,13 +18,13 @@ from app.post.util import post_replies, get_comment_branch, post_reply_count
from app.constants import SUBSCRIPTION_MEMBER, POST_TYPE_LINK, POST_TYPE_IMAGE
from app.models import Post, PostReply, \
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community, \
Topic
Topic, User
from app.post import bp
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
shorten_string, markdown_to_text, gibberish, ap_datetime, return_304, \
request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking, \
reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, moderating_communities, joined_communities, \
blocked_instances, blocked_domains, community_moderators
blocked_instances, blocked_domains, community_moderators, blocked_phrases
def show_post(post_id: int):
@ -47,6 +47,12 @@ def show_post(post_id: int):
mods = community_moderators(community.id)
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
if community.private_mods:
mod_list = []
else:
mod_user_ids = [mod.user_id for mod in mods]
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
# handle top-level comments/replies
form = NewReplyForm()
if current_user.is_authenticated and current_user.verified and form.validate_on_submit():
@ -213,7 +219,7 @@ def show_post(post_id: int):
breadcrumbs.append(breadcrumb)
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
breadcrumbs=breadcrumbs, related_communities=related_communities,
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
@ -409,9 +415,14 @@ def continue_discussion(post_id, comment_id):
abort(404)
mods = post.community.moderators()
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
if post.community.private_mods:
mod_list = []
else:
mod_user_ids = [mod.user_id for mod in mods]
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
replies = get_comment_branch(post.id, comment.id, 'top')
response = render_template('post/continue_discussion.html', title=_('Discussing %(title)s', title=post.title), post=post,
response = render_template('post/continue_discussion.html', title=_('Discussing %(title)s', title=post.title), post=post, mods=mod_list,
is_moderator=is_moderator, comment=comment, replies=replies, markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()), community=post.community,
@ -438,6 +449,11 @@ def add_reply(post_id: int, comment_id: int):
in_reply_to = PostReply.query.get_or_404(comment_id)
mods = post.community.moderators()
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
if post.community.private_mods:
mod_list = []
else:
mod_user_ids = [mod.user_id for mod in mods]
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
if in_reply_to.author.has_blocked_user(current_user.id):
flash(_('You cannot reply to %(name)s', name=in_reply_to.author.display_name()))
@ -466,6 +482,10 @@ def add_reply(post_id: int, comment_id: int):
body_html=markdown_to_html(form.body.data), body_html_safe=True,
from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl,
notify_author=form.notify_author.data, instance_id=1)
if reply.body:
for blocked_phrase in blocked_phrases():
if blocked_phrase in reply.body:
abort(401)
db.session.add(reply)
if in_reply_to.notify_author and current_user.id != in_reply_to.user_id and in_reply_to.author.ap_id is None: # todo: check if replier is blocked
notification = Notification(title=shorten_string(_('Reply from %(name)s on %(post_title)s',
@ -578,7 +598,7 @@ def add_reply(post_id: int, comment_id: int):
form.notify_author.data = True
return render_template('post/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post,
is_moderator=is_moderator, form=form, comment=in_reply_to, markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
moderating_communities=moderating_communities(current_user.get_id()),
moderating_communities=moderating_communities(current_user.get_id()), mods=mod_list,
joined_communities = joined_communities(current_user.id),
inoculation=inoculation[randint(0, len(inoculation) - 1)])
@ -607,6 +627,14 @@ def post_edit(post_id: int):
post = Post.query.get_or_404(post_id)
form = CreatePostForm()
del form.communities
mods = post.community.moderators()
if post.community.private_mods:
mod_list = []
else:
mod_user_ids = [mod.user_id for mod in mods]
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
if post.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin():
if g.site.enable_nsfl is False:
form.nsfl.render_kw = {'disabled': True}
@ -727,7 +755,7 @@ def post_edit(post_id: int):
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
form.sticky.render_kw = {'disabled': True}
return render_template('post/post_edit.html', title=_('Edit post'), form=form, post=post,
markdown_editor=current_user.markdown_editor,
markdown_editor=current_user.markdown_editor, mods=mod_list,
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()),
inoculation=inoculation[randint(0, len(inoculation) - 1)]

View file

@ -15,6 +15,8 @@ def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostRe
instance_ids = blocked_instances(current_user.id)
if instance_ids:
comments = comments.filter(or_(PostReply.instance_id.not_in(instance_ids), PostReply.instance_id == None))
if current_user.ignore_bots:
comments = comments.filter(PostReply.from_bot == False)
if sort_by == 'hot':
comments = comments.order_by(desc(PostReply.ranking))
elif sort_by == 'top':

View file

@ -5,7 +5,8 @@ from sqlalchemy import or_
from app.models import Post
from app.search import bp
from app.utils import moderating_communities, joined_communities, render_template, blocked_domains, blocked_instances
from app.utils import moderating_communities, joined_communities, render_template, blocked_domains, blocked_instances, \
communities_banned_from
@bp.route('/search', methods=['GET', 'POST'])
@ -29,6 +30,9 @@ def run_search():
instance_ids = blocked_instances(current_user.id)
if instance_ids:
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
banned_from = communities_banned_from(current_user.id)
if banned_from:
posts = posts.filter(Post.community_id.not_in(banned_from))
else:
posts = posts.filter(Post.from_bot == False)
posts = posts.filter(Post.nsfl == False)
@ -43,7 +47,7 @@ def run_search():
prev_url = url_for('search.run_search', page=posts.prev_num, q=q) if posts.has_prev and page != 1 else None
return render_template('search/results.html', title=_('Search results for %(q)s', q=q), posts=posts, q=q,
next_url=next_url, prev_url=prev_url,
next_url=next_url, prev_url=prev_url, show_post_community=True,
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()),
site=g.site)

View file

@ -282,6 +282,14 @@ h1 {
}
}
.fe-bot-account {
position: relative;
top: 1px;
&:before {
content: "\e94d";
}
}
.fe-video {
position: relative;
top: 2px;

View file

@ -305,6 +305,14 @@ h1 .fe-bell, h1 .fe-no-bell {
content: "\e986";
}
.fe-bot-account {
position: relative;
top: 1px;
}
.fe-bot-account:before {
content: "\e94d";
}
.fe-video {
position: relative;
top: 2px;

View file

@ -304,6 +304,14 @@ h1 .fe-bell, h1 .fe-no-bell {
content: "\e986";
}
.fe-bot-account {
position: relative;
top: 1px;
}
.fe-bot-account:before {
content: "\e94d";
}
.fe-video {
position: relative;
top: 2px;

View file

@ -17,37 +17,34 @@
<h3>{{ _('Edit %(user_name)s (%(display_name)s)', user_name=user.user_name, display_name=user.display_name()) }}</h3>
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
{{ form.csrf_token() }}
{{ render_field(form.about) }}
{{ render_field(form.email) }}
{{ render_field(form.matrix_user_id) }}
{{ user.about_html|safe if user.about_html }}
<p>Created: {{ moment(user.created).format('MMMM Do YYYY, h:mm:ss a') }}</p>
<p>Last active: {{ moment(user.last_seen).format('MMMM Do YYYY, h:mm:ss a') }}</p>
<p>Email: <a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
<p>Matrix: {{ user.matrix_user_id if user.matrix_user_id }}</p>
{% if user.avatar_id %}
<img class="user_icon_big rounded-circle" src="{{ user.avatar_image() }}" width="120" height="120" />
{% endif %}
{{ render_field(form.profile_file) }}
<small class="field_hint">Provide a square image that looks good when small.</small>
{% if user.cover_id %}
<a href="{{ user.cover_image() }}"><img class="user_icon_big" src="{{ user.cover_image() }}" style="width: 300px; height: auto;" /></a>
{% endif %}
{{ render_field(form.banner_file) }}
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
{{ render_field(form.bot) }}
{{ render_field(form.verified) }}
{{ render_field(form.banned) }}
{{ render_field(form.newsletter) }}
{{ render_field(form.nsfw) }}
{{ render_field(form.nsfl) }}
{{ render_field(form.searchable) }}
{{ render_field(form.indexable) }}
{{ render_field(form.manually_approves_followers) }}
<p>receive newsletter: {{ user.newsletter }}</p>
<p>view nsfw: {{ user.nsfw }}</p>
<p>view nsfl: {{ user.nsfl }}</p>
<p>searchable: {{ user.searchable }}</p>
<p>indexable: {{ user.indexable }}</p>
{{ render_field(form.role) }}
{{ render_field(form.remove_avatar) }}
{{ render_field(form.remove_banner) }}
{{ render_field(form.submit) }}
</form>
<p class="mt-4">
{% if not user.is_local() %}
<a href="{{ user.profile_id() }}" class="btn btn-primary">View original profile</a>
{% endif %}
<a href="" class="btn btn-warning confirm_first">Ban</a>
<a href="" class="btn btn-warning confirm_first">Ban + Purge</a>
</p>
</div>
</div>

View file

@ -11,6 +11,9 @@
{% if user.created_recently() %}
<span class="fe fe-new-account" title="New account"> </span>
{% endif %}
{% if user.bot %}
<span class="fe fe-bot-account" title="Bot account"> </span>
{% endif %}
{% if user.reputation < -10 %}
<span class="fe fe-warning red" title="Very low reputation. Beware."> </span>
<span class="fe fe-warning red" title="Very low reputation. Beware!"> </span>

View file

@ -48,11 +48,12 @@
<td>{{ user.reports if user.reports > 0 }} </td>
<td>{{ user.ip_address if user.ip_address }} </td>
<td>{% if user.is_local() %}
<a href="/u/{{ user.link() }}">View local</a>
<a href="/u/{{ user.link() }}">View</a>
{% else %}
<a href="/u/{{ user.link() }}">View local</a> |
<a href="{{ user.ap_profile_id }}">View remote</a>
{% endif %}
| <a href="#" class="confirm_first">{{ _('Un ban') }}</a>
| <a href="{{ url_for('community.community_unban_user', community_id=community.id, user_id=user.id) }}" class="confirm_first">{{ _('Un ban') }}</a>
</td>
</tr>
{% endfor %}

View file

@ -29,6 +29,9 @@
{% if comment['comment'].author.created_recently() %}
<span class="fe fe-new-account small" title="New account"> </span>
{% endif %}
{% if comment['comment'].author.bot %}
<span class="fe fe-bot-account small" title="Bot account"> </span>
{% endif %}
{% if comment['comment'].author.id != current_user.id %}
{% if comment['comment'].author.reputation < -10 %}
<span class="fe fe-warning red" title="Very low reputation. Beware."> </span>

View file

@ -90,6 +90,9 @@
{% if comment['comment'].author.created_recently() %}
<span class="fe fe-new-account small" title="New account"> </span>
{% endif %}
{% if comment['comment'].author.bot %}
<span class="fe fe-bot-account small" title="Bot account"> </span>
{% endif %}
{% if comment['comment'].author.id != current_user.id %}
{% if comment['comment'].author.reputation < -10 %}
<span class="fe fe-warning red" title="Very low reputation. Beware."> </span>

View file

@ -57,6 +57,9 @@
</div>
{% endif %}
<p class="small">{{ _('Joined') }}: {{ moment(user.created).fromNow(refresh=True) }}<br />
{% if user.bot %}
{{ _('Bot Account') }}<br />
{% endif %}
{{ _('Attitude') }}: <span title="{{ _('Ratio of upvotes cast to downvotes cast. Higher is more positive.') }}">{{ (user.attitude * 100) | round | int }}%</span></p>
{{ user.about_html|safe }}
{% if posts %}

View file

@ -16,7 +16,8 @@ from app.topic import bp
from app import db, celery, cache
from app.topic.forms import ChooseTopicsForm
from app.utils import render_template, user_filters_posts, moderating_communities, joined_communities, \
community_membership, blocked_domains, validation_required, mimetype_from_url, blocked_instances
community_membership, blocked_domains, validation_required, mimetype_from_url, blocked_instances, \
communities_banned_from
@bp.route('/topic/<path:topic_path>', methods=['GET'])
@ -68,6 +69,9 @@ def show_topic(topic_path):
instance_ids = blocked_instances(current_user.id)
if instance_ids:
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
banned_from = communities_banned_from(current_user.id)
if banned_from:
posts = posts.filter(Post.community_id.not_in(banned_from))
# sorting
if sort == '' or sort == 'hot':

View file

@ -79,7 +79,7 @@ def show_profile(user):
description=description, subscribed=subscribed, upvoted=upvoted,
post_next_url=post_next_url, post_prev_url=post_prev_url,
replies_next_url=replies_next_url, replies_prev_url=replies_prev_url,
noindex=not user.indexable,
noindex=not user.indexable, show_post_community=True,
moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id())
)

View file

@ -149,7 +149,7 @@ def search_for_user(address: str):
if user_data.status_code == 200:
user_json = user_data.json()
user_data.close()
if user_json['type'] == 'Person':
if user_json['type'] == 'Person' or user_json['type'] == 'Service':
user = actor_json_to_model(user_json, name, server)
return user
return None

View file

@ -28,7 +28,7 @@ import re
from app.email import send_welcome_email
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock
Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan
# Flask's render_template function, with support for themes added
@ -251,7 +251,7 @@ def html_to_markdown_worker(element, indent_level=0):
def markdown_to_html(markdown_text) -> str:
if markdown_text:
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'spoiler': True}))
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'strike': True}))
else:
return ''
@ -312,6 +312,12 @@ def community_membership(user: User, community: Community) -> int:
return user.subscribed(community.id)
@cache.memoize(timeout=86400)
def communities_banned_from(user_id) -> List[int]:
community_bans = CommunityBan.query.filter(CommunityBan.user_id == user_id).all()
return [cb.community_id for cb in community_bans]
@cache.memoize(timeout=86400)
def blocked_domains(user_id) -> List[int]:
blocks = DomainBlock.query.filter_by(user_id=user_id)
@ -324,6 +330,23 @@ def blocked_instances(user_id) -> List[int]:
return [block.instance_id for block in blocks]
@cache.memoize(timeout=86400)
def blocked_phrases() -> List[str]:
site = Site.query.get(1)
if site.blocked_phrases:
return [phrase for phrase in site.blocked_phrases.split('\n') if phrase != '']
else:
return []
@cache.memoize(timeout=86400)
def blocked_referrers() -> List[str]:
site = Site.query.get(1)
if site.auto_decline_referrers:
return [referrer for referrer in site.auto_decline_referrers.split('\n') if referrer != '']
else:
return []
def retrieve_block_list():
try:
response = requests.get('https://raw.githubusercontent.com/rimu/no-qanon/master/domains.txt', timeout=1)
@ -442,7 +465,7 @@ def banned_ip_addresses() -> List[str]:
def can_downvote(user, community: Community, site=None) -> bool:
if user is None or community is None or user.banned:
if user is None or community is None or user.banned or user.bot:
return False
if site is None:
@ -460,11 +483,17 @@ def can_downvote(user, community: Community, site=None) -> bool:
if user.attitude < -0.40 or user.reputation < -10: # this should exclude about 3.7% of users.
return False
if community.id in communities_banned_from(user.id):
return False
return True
def can_upvote(user, community: Community) -> bool:
if user is None or community is None or user.banned:
if user is None or community is None or user.banned or user.bot:
return False
if community.id in communities_banned_from(user.id):
return False
return True
@ -483,6 +512,9 @@ def can_create_post(user, content: Community) -> bool:
if content.local_only and not user.is_local():
return False
if content.id in communities_banned_from(user.id):
return False
return True
@ -496,6 +528,9 @@ def can_create_post_reply(user, content: Community) -> bool:
if content.local_only and not user.is_local():
return False
if content.id in communities_banned_from(user.id):
return False
return True
@ -602,7 +637,8 @@ def moderating_communities(user_id):
return []
return Community.query.join(CommunityMember, Community.id == CommunityMember.community_id).\
filter(Community.banned == False).\
filter(or_(CommunityMember.is_moderator == True, CommunityMember.is_owner == True)).\
filter(or_(CommunityMember.is_moderator == True, CommunityMember.is_owner == True)). \
filter(CommunityMember.is_banned == False). \
filter(CommunityMember.user_id == user_id).order_by(Community.title).all()
@ -613,6 +649,7 @@ def joined_communities(user_id):
return Community.query.join(CommunityMember, Community.id == CommunityMember.community_id).\
filter(Community.banned == False). \
filter(CommunityMember.is_moderator == False, CommunityMember.is_owner == False). \
filter(CommunityMember.is_banned == False). \
filter(CommunityMember.user_id == user_id).order_by(Community.title).all()

View file

@ -42,3 +42,10 @@ class Config(object):
SENTRY_DSN = os.environ.get('SENTRY_DSN') or None
AWS_REGION = os.environ.get('AWS_REGION') or None
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
CLOUDFLARE_API_TOKEN = os.environ.get('CLOUDFLARE_API_TOKEN') or ''
CLOUDFLARE_ZONE_ID = os.environ.get('CLOUDFLARE_ZONE_ID') or ''

View file

@ -26,3 +26,6 @@ FLASK_APP = 'pyfedi.py'
SENTRY_DSN = ''
AWS_REGION = 'ap-southeast-2'
CLOUDFLARE_API_TOKEN = ''
CLOUDFLARE_ZONE_ID = ''

View file

@ -0,0 +1,32 @@
"""blocked phrases
Revision ID: 2b028a70bd7a
Revises: 12d60b9d5417
Create Date: 2024-03-22 11:50:15.405786
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2b028a70bd7a'
down_revision = '12d60b9d5417'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('site', schema=None) as batch_op:
batch_op.add_column(sa.Column('blocked_phrases', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('site', schema=None) as batch_op:
batch_op.drop_column('blocked_phrases')
# ### end Alembic commands ###

View file

@ -30,3 +30,4 @@ redis==5.0.1
Werkzeug==2.3.3
pytesseract==0.3.10
sentry-sdk==1.40.6
urllib3==1.26.1