mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-24 11:51:27 -08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
0ecdcd4d1e
31 changed files with 430 additions and 126 deletions
16
INSTALL.md
16
INSTALL.md
|
@ -18,7 +18,11 @@
|
||||||
|
|
||||||
## Setup Database
|
## 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:
|
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'.
|
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
|
#### 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.
|
To use SMTP you need to set all the MAIL_* environment variables in you .env file. See env.sample for a list of them.
|
||||||
|
|
|
@ -195,7 +195,7 @@ def user_profile(actor):
|
||||||
if is_activitypub_request():
|
if is_activitypub_request():
|
||||||
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" if not user.bot else "Service",
|
||||||
"id": f"https://{server}/u/{actor}",
|
"id": f"https://{server}/u/{actor}",
|
||||||
"preferredUsername": actor,
|
"preferredUsername": actor,
|
||||||
"name": user.title if user.title else user.user_name,
|
"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",
|
"outbox": f"https://{server}/u/{actor}/outbox",
|
||||||
"discoverable": user.searchable,
|
"discoverable": user.searchable,
|
||||||
"indexable": user.indexable,
|
"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": {
|
"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}",
|
||||||
|
@ -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!
|
target_ap_id = request_json['object']['object']['object'] # object object object!
|
||||||
post = undo_vote(activity_log, comment, post, target_ap_id, user)
|
post = undo_vote(activity_log, comment, post, target_ap_id, user)
|
||||||
activity_log.result = 'success'
|
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']
|
activity_log.activity_type = request_json['object']['type']
|
||||||
featured_url = Community.query.filter(Community.ap_public_url == request_json['actor']).first().ap_featured_url
|
target = request_json['object']['target']
|
||||||
if featured_url:
|
community = Community.query.filter_by(ap_public_url=request_json['actor']).first()
|
||||||
if 'target' in request_json['object'] and featured_url == request_json['object']['target']:
|
if community:
|
||||||
post = Post.query.filter(Post.ap_id == request_json['object']['object']).first()
|
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:
|
if post:
|
||||||
post.sticky = True
|
post.sticky = True
|
||||||
activity_log.result = 'success'
|
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']
|
activity_log.activity_type = request_json['object']['type']
|
||||||
featured_url = Community.query.filter(Community.ap_public_url == request_json['actor']).first().ap_featured_url
|
target = request_json['object']['target']
|
||||||
if featured_url:
|
community = Community.query.filter_by(ap_public_url=request_json['actor']).first()
|
||||||
if 'target' in request_json['object'] and featured_url == request_json['object']['target']:
|
if community:
|
||||||
post = Post.query.filter(Post.ap_id == request_json['object']['object']).first()
|
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:
|
if post:
|
||||||
post.sticky = False
|
post.sticky = False
|
||||||
activity_log.result = 'success'
|
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:
|
else:
|
||||||
activity_log.exception_message = 'Invalid type for Announce'
|
activity_log.exception_message = 'Invalid type for Announce'
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,8 @@ import pytesseract
|
||||||
|
|
||||||
from app.utils import get_request, allowlist_html, html_to_markdown, get_setting, ap_datetime, markdown_to_html, \
|
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, \
|
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():
|
def public_key():
|
||||||
|
@ -488,7 +489,7 @@ def refresh_community_profile_task(community_id):
|
||||||
|
|
||||||
|
|
||||||
def actor_json_to_model(activity_json, address, server):
|
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:
|
try:
|
||||||
user = User(user_name=activity_json['preferredUsername'],
|
user = User(user_name=activity_json['preferredUsername'],
|
||||||
title=activity_json['name'] if 'name' in activity_json else None,
|
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_fetched_at=utcnow(),
|
||||||
ap_domain=server,
|
ap_domain=server,
|
||||||
public_key=activity_json['publicKey']['publicKeyPem'],
|
public_key=activity_json['publicKey']['publicKeyPem'],
|
||||||
|
bot=True if activity_json['type'] == 'Service' else False,
|
||||||
instance_id=find_instance_id(server)
|
instance_id=find_instance_id(server)
|
||||||
# language=community_json['language'][0]['identifier'] # todo: language
|
# 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,
|
root_id=root_id,
|
||||||
nsfw=community.nsfw,
|
nsfw=community.nsfw,
|
||||||
nsfl=community.nsfl,
|
nsfl=community.nsfl,
|
||||||
|
from_bot=user.bot,
|
||||||
up_votes=1,
|
up_votes=1,
|
||||||
depth=depth,
|
depth=depth,
|
||||||
score=instance_weight(user.ap_domain),
|
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 = allowlist_html(request_json['object']['content'])
|
||||||
post_reply.body = html_to_markdown(post_reply.body_html)
|
post_reply.body = html_to_markdown(post_reply.body_html)
|
||||||
if post_id is not None:
|
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)
|
post = Post.query.get(post_id)
|
||||||
if post.comments_enabled:
|
if post.comments_enabled:
|
||||||
anchor = None
|
anchor = None
|
||||||
|
@ -1256,6 +1264,7 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
||||||
ap_announce_id=announce_id,
|
ap_announce_id=announce_id,
|
||||||
type=constants.POST_TYPE_ARTICLE,
|
type=constants.POST_TYPE_ARTICLE,
|
||||||
up_votes=1,
|
up_votes=1,
|
||||||
|
from_bot=user.bot,
|
||||||
score=instance_weight(user.ap_domain),
|
score=instance_weight(user.ap_domain),
|
||||||
instance_id=user.instance_id,
|
instance_id=user.instance_id,
|
||||||
indexable=user.indexable
|
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
|
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 = allowlist_html(request_json['object']['content'])
|
||||||
post.body = html_to_markdown(post.body_html)
|
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 \
|
if 'attachment' in request_json['object'] and len(request_json['object']['attachment']) > 0 and \
|
||||||
'type' in request_json['object']['attachment'][0]:
|
'type' in request_json['object']['attachment'][0]:
|
||||||
if request_json['object']['attachment'][0]['type'] == 'Link':
|
if request_json['object']['attachment'][0]['type'] == 'Link':
|
||||||
|
|
|
@ -31,6 +31,7 @@ class SiteMiscForm(FlaskForm):
|
||||||
types = [('Open', _l('Open')), ('RequireApplication', _l('Require application')), ('Closed', _l('Closed'))]
|
types = [('Open', _l('Open')), ('RequireApplication', _l('Require application')), ('Closed', _l('Closed'))]
|
||||||
registration_mode = SelectField(_l('Registration mode'), choices=types, default=1, coerce=str)
|
registration_mode = SelectField(_l('Registration mode'), choices=types, default=1, coerce=str)
|
||||||
application_question = TextAreaField(_l('Question to ask people applying for an account'))
|
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'))
|
log_activitypub_json = BooleanField(_l('Log ActivityPub JSON for debugging'))
|
||||||
default_theme = SelectField(_l('Default theme'), coerce=str)
|
default_theme = SelectField(_l('Default theme'), coerce=str)
|
||||||
submit = SubmitField(_l('Save'))
|
submit = SubmitField(_l('Save'))
|
||||||
|
@ -41,6 +42,7 @@ class FederationForm(FlaskForm):
|
||||||
allowlist = TextAreaField(_l('Allow federation with these instances'))
|
allowlist = TextAreaField(_l('Allow federation with these instances'))
|
||||||
use_blocklist = BooleanField(_l('Blocklist instead of allowlist'))
|
use_blocklist = BooleanField(_l('Blocklist instead of allowlist'))
|
||||||
blocklist = TextAreaField(_l('Deny federation with these instances'))
|
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'))
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -165,26 +167,16 @@ class AddUserForm(FlaskForm):
|
||||||
|
|
||||||
|
|
||||||
class EditUserForm(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'))
|
bot = BooleanField(_l('This profile is a bot'))
|
||||||
verified = BooleanField(_l('Email address is verified'))
|
verified = BooleanField(_l('Email address is verified'))
|
||||||
banned = BooleanField(_l('Banned'))
|
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')),
|
role_options = [(2, _l('User')),
|
||||||
(3, _l('Staff')),
|
(3, _l('Staff')),
|
||||||
(4, _l('Admin')),
|
(4, _l('Admin')),
|
||||||
]
|
]
|
||||||
role = SelectField(_l('Role'), choices=role_options, default=2, coerce=int)
|
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'))
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,10 @@ from flask_login import login_required, current_user
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from sqlalchemy import text, desc
|
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.routes import process_inbox_request, process_delete_request
|
||||||
from app.activitypub.signature import post_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, \
|
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \
|
||||||
EditTopicForm, SendNewsletterForm, AddUserForm
|
EditTopicForm, SendNewsletterForm, AddUserForm
|
||||||
from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community, send_newsletter, \
|
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, \
|
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
|
||||||
User, Instance, File, Report, Topic, UserRegistration, Role, Post
|
User, Instance, File, Report, Topic, UserRegistration, Role, Post
|
||||||
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \
|
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
|
from app.admin import bp
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,12 +80,14 @@ def admin_misc():
|
||||||
site.reports_email_admins = form.reports_email_admins.data
|
site.reports_email_admins = form.reports_email_admins.data
|
||||||
site.registration_mode = form.registration_mode.data
|
site.registration_mode = form.registration_mode.data
|
||||||
site.application_question = form.application_question.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.log_activitypub_json = form.log_activitypub_json.data
|
||||||
site.updated = utcnow()
|
site.updated = utcnow()
|
||||||
site.default_theme = form.default_theme.data
|
site.default_theme = form.default_theme.data
|
||||||
if site.id is None:
|
if site.id is None:
|
||||||
db.session.add(site)
|
db.session.add(site)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
cache.delete_memoized(blocked_referrers)
|
||||||
flash('Settings saved.')
|
flash('Settings saved.')
|
||||||
elif request.method == 'GET':
|
elif request.method == 'GET':
|
||||||
form.enable_downvotes.data = site.enable_downvotes
|
form.enable_downvotes.data = site.enable_downvotes
|
||||||
|
@ -97,6 +99,7 @@ def admin_misc():
|
||||||
form.reports_email_admins.data = site.reports_email_admins
|
form.reports_email_admins.data = site.reports_email_admins
|
||||||
form.registration_mode.data = site.registration_mode
|
form.registration_mode.data = site.registration_mode
|
||||||
form.application_question.data = site.application_question
|
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.log_activitypub_json.data = site.log_activitypub_json
|
||||||
form.default_theme.data = site.default_theme if site.default_theme is not None else ''
|
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,
|
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'):
|
for allow in form.allowlist.data.split('\n'):
|
||||||
if allow.strip():
|
if allow.strip():
|
||||||
db.session.add(AllowedInstances(domain=allow.strip()))
|
db.session.add(AllowedInstances(domain=allow.strip()))
|
||||||
|
cache.delete_memoized(instance_allowed, allow.strip())
|
||||||
if form.use_blocklist.data:
|
if form.use_blocklist.data:
|
||||||
set_setting('use_allowlist', False)
|
set_setting('use_allowlist', False)
|
||||||
db.session.execute(text('DELETE FROM banned_instances'))
|
db.session.execute(text('DELETE FROM banned_instances'))
|
||||||
for banned in form.blocklist.data.split('\n'):
|
for banned in form.blocklist.data.split('\n'):
|
||||||
if banned.strip():
|
if banned.strip():
|
||||||
db.session.add(BannedInstances(domain=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()
|
db.session.commit()
|
||||||
|
|
||||||
flash(_('Admin settings saved'))
|
flash(_('Admin settings saved'))
|
||||||
|
|
||||||
elif request.method == 'GET':
|
elif request.method == 'GET':
|
||||||
|
@ -139,6 +147,7 @@ def admin_federation():
|
||||||
form.blocklist.data = '\n'.join([instance.domain for instance in instances])
|
form.blocklist.data = '\n'.join([instance.domain for instance in instances])
|
||||||
instances = AllowedInstances.query.all()
|
instances = AllowedInstances.query.all()
|
||||||
form.allowlist.data = '\n'.join([instance.domain for instance in instances])
|
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,
|
return render_template('admin/federation.html', title=_('Federation settings'), form=form,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
@ -526,46 +535,20 @@ def admin_user_edit(user_id):
|
||||||
form = EditUserForm()
|
form = EditUserForm()
|
||||||
user = User.query.get_or_404(user_id)
|
user = User.query.get_or_404(user_id)
|
||||||
if form.validate_on_submit():
|
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.bot = form.bot.data
|
||||||
user.verified = form.verified.data
|
user.verified = form.verified.data
|
||||||
user.banned = form.banned.data
|
user.banned = form.banned.data
|
||||||
profile_file = request.files['profile_file']
|
if form.remove_avatar.data and user.avatar_id:
|
||||||
if profile_file and profile_file.filename != '':
|
file = File.query.get(user.avatar_id)
|
||||||
# remove old avatar
|
file.delete_from_disk()
|
||||||
if user.avatar_id:
|
user.avatar_id = None
|
||||||
file = File.query.get(user.avatar_id)
|
db.session.delete(file)
|
||||||
file.delete_from_disk()
|
|
||||||
user.avatar_id = None
|
|
||||||
db.session.delete(file)
|
|
||||||
|
|
||||||
# add new avatar
|
if form.remove_banner.data and user.cover_id:
|
||||||
file = save_icon_file(profile_file, 'users')
|
file = File.query.get(user.cover_id)
|
||||||
if file:
|
file.delete_from_disk()
|
||||||
user.avatar = file
|
user.cover_id = None
|
||||||
banner_file = request.files['banner_file']
|
db.session.delete(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
|
|
||||||
|
|
||||||
# Update user roles. The UI only lets the user choose 1 role but the DB structure allows for multiple roles per user.
|
# 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})
|
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:
|
else:
|
||||||
if not user.is_local():
|
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')
|
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.bot.data = user.bot
|
||||||
form.verified.data = user.verified
|
form.verified.data = user.verified
|
||||||
form.banned.data = user.banned
|
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:
|
if user.roles and user.roles.count() > 0:
|
||||||
form.role.data = user.roles[0].id
|
form.role.data = user.roles[0].id
|
||||||
|
|
||||||
|
|
|
@ -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.email import send_verification_email, send_password_reset_email
|
||||||
from app.models import User, utcnow, IpBan, UserRegistration, Notification, Site
|
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, \
|
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'])
|
@bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
@ -98,6 +98,11 @@ def register():
|
||||||
if form.user_name.data in disallowed_usernames:
|
if form.user_name.data in disallowed_usernames:
|
||||||
flash(_('Sorry, you cannot use that user name'), 'error')
|
flash(_('Sorry, you cannot use that user name'), 'error')
|
||||||
else:
|
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)
|
verification_token = random_token(16)
|
||||||
form.user_name.data = form.user_name.data.strip()
|
form.user_name.data = form.user_name.data.strip()
|
||||||
before_normalize = form.user_name.data
|
before_normalize = form.user_name.data
|
||||||
|
|
|
@ -68,7 +68,7 @@ class SearchRemoteCommunity(FlaskForm):
|
||||||
|
|
||||||
class BanUserCommunityForm(FlaskForm):
|
class BanUserCommunityForm(FlaskForm):
|
||||||
reason = StringField(_l('Reason'), render_kw={'autofocus': True}, validators=[DataRequired()])
|
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_posts = BooleanField(_l('Also delete all their posts'))
|
||||||
delete_post_replies = BooleanField(_l('Also delete all their comments'))
|
delete_post_replies = BooleanField(_l('Also delete all their comments'))
|
||||||
submit = SubmitField(_l('Ban'))
|
submit = SubmitField(_l('Ban'))
|
||||||
|
|
|
@ -27,7 +27,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
|
||||||
shorten_string, gibberish, community_membership, ap_datetime, \
|
shorten_string, gibberish, community_membership, ap_datetime, \
|
||||||
request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \
|
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, \
|
joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \
|
||||||
community_moderators
|
community_moderators, communities_banned_from
|
||||||
from feedgen.feed import FeedGenerator
|
from feedgen.feed import FeedGenerator
|
||||||
from datetime import timezone, timedelta
|
from datetime import timezone, timedelta
|
||||||
|
|
||||||
|
@ -793,6 +793,9 @@ def community_ban_user(community_id: int, user_id: int):
|
||||||
|
|
||||||
form = BanUserCommunityForm()
|
form = BanUserCommunityForm()
|
||||||
if form.validate_on_submit():
|
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:
|
if not existing:
|
||||||
new_ban = CommunityBan(community_id=community_id, user_id=user.id, banned_by=current_user.id,
|
new_ban = CommunityBan(community_id=community_id, user_id=user.id, banned_by=current_user.id,
|
||||||
reason=form.reason.data)
|
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
|
new_ban.ban_until = form.ban_until.data
|
||||||
db.session.add(new_ban)
|
db.session.add(new_ban)
|
||||||
db.session.commit()
|
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:
|
if form.delete_posts.data:
|
||||||
posts = Post.query.filter(Post.user_id == user.id, Post.community_id == community.id).all()
|
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
|
# notify banned person
|
||||||
if user.is_local():
|
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),
|
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)
|
author_id=1)
|
||||||
db.session.add(notify)
|
db.session.add(notify)
|
||||||
user.unread_notifications += 1
|
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'])
|
@bp.route('/<int:community_id>/notification', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
@ -16,7 +16,7 @@ from app.models import Community, File, BannedInstances, PostReply, PostVote, Po
|
||||||
Instance, Notification, User, ActivityPubLog
|
Instance, Notification, User, ActivityPubLog
|
||||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
|
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, \
|
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
|
from sqlalchemy import func, desc
|
||||||
import os
|
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
|
c.last_active = Post.query.filter(Post.community_id == community_id).order_by(desc(Post.posted_at)).first().posted_at
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
if community.ap_featured_url:
|
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:
|
if featured_request.status_code == 200:
|
||||||
featured_data = featured_request.json()
|
featured_data = featured_request.json()
|
||||||
featured_request.close()
|
featured_request.close()
|
||||||
|
@ -299,6 +299,19 @@ def save_post(form, post: Post):
|
||||||
if current_user.reputation < -100:
|
if current_user.reputation < -100:
|
||||||
post.score = -1
|
post.score = -1
|
||||||
post.ranking = post_ranking(post.score, utcnow())
|
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)
|
db.session.add(post)
|
||||||
|
|
||||||
g.site.last_active = utcnow()
|
g.site.last_active = utcnow()
|
||||||
|
|
|
@ -5,6 +5,7 @@ from random import randint
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import markdown2
|
import markdown2
|
||||||
|
import requests
|
||||||
from sqlalchemy.sql.operators import or_, and_
|
from sqlalchemy.sql.operators import or_, and_
|
||||||
|
|
||||||
from app import db, cache
|
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, \
|
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, \
|
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, \
|
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, \
|
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \
|
||||||
InstanceRole, Notification
|
InstanceRole, Notification
|
||||||
from PIL import Image
|
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
|
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
|
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,
|
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,
|
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
|
||||||
|
@ -178,6 +185,11 @@ def list_communities():
|
||||||
if topic_id != 0:
|
if topic_id != 0:
|
||||||
communities = communities.filter_by(topic_id=topic_id)
|
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'),
|
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_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||||
|
@ -265,8 +277,7 @@ def list_files(directory):
|
||||||
|
|
||||||
@bp.route('/test')
|
@bp.route('/test')
|
||||||
def test():
|
def test():
|
||||||
x = find_actor_or_create('artporn@lemm.ee')
|
return ''
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
users_to_notify = User.query.join(Notification, User.id == Notification.user_id).filter(
|
users_to_notify = User.query.join(Notification, User.id == Notification.user_id).filter(
|
||||||
User.ap_id == None,
|
User.ap_id == None,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from datetime import datetime, timedelta, date, timezone
|
from datetime import datetime, timedelta, date, timezone
|
||||||
from time import time
|
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 import current_app, escape, url_for, render_template_string
|
||||||
from flask_login import UserMixin, current_user
|
from flask_login import UserMixin, current_user
|
||||||
from sqlalchemy import or_, text
|
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 sqlalchemy_utils.types import TSVectorType # https://sqlalchemy-searchable.readthedocs.io/en/latest/installation.html
|
||||||
from flask_sqlalchemy import BaseQuery
|
from flask_sqlalchemy import BaseQuery
|
||||||
from sqlalchemy_searchable import SearchQueryMixin
|
from sqlalchemy_searchable import SearchQueryMixin
|
||||||
from app import db, login, cache
|
from app import db, login, cache, celery
|
||||||
import jwt
|
import jwt
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -203,12 +204,20 @@ class File(db.Model):
|
||||||
return f"https://{current_app.config['SERVER_NAME']}/{thumbnail_path}"
|
return f"https://{current_app.config['SERVER_NAME']}/{thumbnail_path}"
|
||||||
|
|
||||||
def delete_from_disk(self):
|
def delete_from_disk(self):
|
||||||
|
purge_from_cache = []
|
||||||
if self.file_path and os.path.isfile(self.file_path):
|
if self.file_path and os.path.isfile(self.file_path):
|
||||||
os.unlink(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):
|
if self.thumbnail_path and os.path.isfile(self.thumbnail_path):
|
||||||
os.unlink(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):
|
if self.source_url and not self.source_url.startswith('http') and os.path.isfile(self.source_url):
|
||||||
os.unlink(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):
|
def filesize(self):
|
||||||
size = 0
|
size = 0
|
||||||
|
@ -219,6 +228,50 @@ class File(db.Model):
|
||||||
return size
|
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):
|
class Topic(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
machine_name = db.Column(db.String(50), index=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_fetched_at = db.Column(db.DateTime)
|
||||||
ap_followers_url = db.Column(db.String(255))
|
ap_followers_url = db.Column(db.String(255))
|
||||||
ap_preferred_username = 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_deleted_at = db.Column(db.DateTime)
|
||||||
ap_inbox_url = db.Column(db.String(255))
|
ap_inbox_url = db.Column(db.String(255))
|
||||||
ap_domain = 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):
|
def num_content(self):
|
||||||
content = 0
|
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" 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 = ' + str(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
|
return content
|
||||||
|
|
||||||
def is_local(self):
|
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
|
allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list
|
||||||
allowlist = db.Column(db.Text, default='')
|
allowlist = db.Column(db.Text, default='')
|
||||||
blocklist = 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)
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
updated = db.Column(db.DateTime, default=utcnow)
|
updated = db.Column(db.DateTime, default=utcnow)
|
||||||
last_active = db.Column(db.DateTime, default=utcnow)
|
last_active = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
|
@ -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.constants import SUBSCRIPTION_MEMBER, POST_TYPE_LINK, POST_TYPE_IMAGE
|
||||||
from app.models import Post, PostReply, \
|
from app.models import Post, PostReply, \
|
||||||
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community, \
|
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community, \
|
||||||
Topic
|
Topic, User
|
||||||
from app.post import bp
|
from app.post 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, gibberish, ap_datetime, return_304, \
|
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, \
|
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, \
|
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):
|
def show_post(post_id: int):
|
||||||
|
@ -47,6 +47,12 @@ def show_post(post_id: int):
|
||||||
mods = community_moderators(community.id)
|
mods = community_moderators(community.id)
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
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
|
# handle top-level comments/replies
|
||||||
form = NewReplyForm()
|
form = NewReplyForm()
|
||||||
if current_user.is_authenticated and current_user.verified and form.validate_on_submit():
|
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)
|
breadcrumbs.append(breadcrumb)
|
||||||
|
|
||||||
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
|
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,
|
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,
|
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,
|
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)
|
abort(404)
|
||||||
mods = post.community.moderators()
|
mods = post.community.moderators()
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
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')
|
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,
|
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()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()), community=post.community,
|
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)
|
in_reply_to = PostReply.query.get_or_404(comment_id)
|
||||||
mods = post.community.moderators()
|
mods = post.community.moderators()
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
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):
|
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()))
|
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,
|
body_html=markdown_to_html(form.body.data), body_html_safe=True,
|
||||||
from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl,
|
from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl,
|
||||||
notify_author=form.notify_author.data, instance_id=1)
|
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)
|
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
|
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',
|
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
|
form.notify_author.data = True
|
||||||
return render_template('post/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
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,
|
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),
|
joined_communities = joined_communities(current_user.id),
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
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)
|
post = Post.query.get_or_404(post_id)
|
||||||
form = CreatePostForm()
|
form = CreatePostForm()
|
||||||
del form.communities
|
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 post.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin():
|
||||||
if g.site.enable_nsfl is False:
|
if g.site.enable_nsfl is False:
|
||||||
form.nsfl.render_kw = {'disabled': True}
|
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()):
|
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||||
form.sticky.render_kw = {'disabled': True}
|
form.sticky.render_kw = {'disabled': True}
|
||||||
return render_template('post/post_edit.html', title=_('Edit post'), form=form, post=post,
|
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()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()),
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||||
|
|
|
@ -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)
|
instance_ids = blocked_instances(current_user.id)
|
||||||
if instance_ids:
|
if instance_ids:
|
||||||
comments = comments.filter(or_(PostReply.instance_id.not_in(instance_ids), PostReply.instance_id == None))
|
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':
|
if sort_by == 'hot':
|
||||||
comments = comments.order_by(desc(PostReply.ranking))
|
comments = comments.order_by(desc(PostReply.ranking))
|
||||||
elif sort_by == 'top':
|
elif sort_by == 'top':
|
||||||
|
|
|
@ -5,7 +5,8 @@ from sqlalchemy import or_
|
||||||
|
|
||||||
from app.models import Post
|
from app.models import Post
|
||||||
from app.search import bp
|
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'])
|
@bp.route('/search', methods=['GET', 'POST'])
|
||||||
|
@ -29,6 +30,9 @@ def run_search():
|
||||||
instance_ids = blocked_instances(current_user.id)
|
instance_ids = blocked_instances(current_user.id)
|
||||||
if instance_ids:
|
if instance_ids:
|
||||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
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:
|
else:
|
||||||
posts = posts.filter(Post.from_bot == False)
|
posts = posts.filter(Post.from_bot == False)
|
||||||
posts = posts.filter(Post.nsfl == 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
|
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,
|
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()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()),
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
site=g.site)
|
site=g.site)
|
||||||
|
|
|
@ -282,6 +282,14 @@ h1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-bot-account {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
&:before {
|
||||||
|
content: "\e94d";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fe-video {
|
.fe-video {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
|
|
@ -305,6 +305,14 @@ h1 .fe-bell, h1 .fe-no-bell {
|
||||||
content: "\e986";
|
content: "\e986";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-bot-account {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.fe-bot-account:before {
|
||||||
|
content: "\e94d";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-video {
|
.fe-video {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
|
|
@ -304,6 +304,14 @@ h1 .fe-bell, h1 .fe-no-bell {
|
||||||
content: "\e986";
|
content: "\e986";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-bot-account {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.fe-bot-account:before {
|
||||||
|
content: "\e94d";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-video {
|
.fe-video {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
|
|
|
@ -17,37 +17,34 @@
|
||||||
<h3>{{ _('Edit %(user_name)s (%(display_name)s)', user_name=user.user_name, display_name=user.display_name()) }}</h3>
|
<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 method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||||
{{ form.csrf_token() }}
|
{{ form.csrf_token() }}
|
||||||
{{ render_field(form.about) }}
|
{{ user.about_html|safe if user.about_html }}
|
||||||
{{ render_field(form.email) }}
|
<p>Created: {{ moment(user.created).format('MMMM Do YYYY, h:mm:ss a') }}</p>
|
||||||
{{ render_field(form.matrix_user_id) }}
|
<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 %}
|
{% if user.avatar_id %}
|
||||||
<img class="user_icon_big rounded-circle" src="{{ user.avatar_image() }}" width="120" height="120" />
|
<img class="user_icon_big rounded-circle" src="{{ user.avatar_image() }}" width="120" height="120" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ render_field(form.profile_file) }}
|
|
||||||
<small class="field_hint">Provide a square image that looks good when small.</small>
|
|
||||||
{% if user.cover_id %}
|
{% if user.cover_id %}
|
||||||
<a href="{{ user.cover_image() }}"><img class="user_icon_big" src="{{ user.cover_image() }}" style="width: 300px; height: auto;" /></a>
|
<a href="{{ user.cover_image() }}"><img class="user_icon_big" src="{{ user.cover_image() }}" style="width: 300px; height: auto;" /></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ render_field(form.banner_file) }}
|
|
||||||
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
|
|
||||||
{{ render_field(form.bot) }}
|
{{ render_field(form.bot) }}
|
||||||
{{ render_field(form.verified) }}
|
{{ render_field(form.verified) }}
|
||||||
{{ render_field(form.banned) }}
|
{{ render_field(form.banned) }}
|
||||||
{{ render_field(form.newsletter) }}
|
<p>receive newsletter: {{ user.newsletter }}</p>
|
||||||
{{ render_field(form.nsfw) }}
|
<p>view nsfw: {{ user.nsfw }}</p>
|
||||||
{{ render_field(form.nsfl) }}
|
<p>view nsfl: {{ user.nsfl }}</p>
|
||||||
{{ render_field(form.searchable) }}
|
<p>searchable: {{ user.searchable }}</p>
|
||||||
{{ render_field(form.indexable) }}
|
<p>indexable: {{ user.indexable }}</p>
|
||||||
{{ render_field(form.manually_approves_followers) }}
|
|
||||||
{{ render_field(form.role) }}
|
{{ render_field(form.role) }}
|
||||||
|
{{ render_field(form.remove_avatar) }}
|
||||||
|
{{ render_field(form.remove_banner) }}
|
||||||
{{ render_field(form.submit) }}
|
{{ render_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
{% if not user.is_local() %}
|
{% if not user.is_local() %}
|
||||||
<a href="{{ user.profile_id() }}" class="btn btn-primary">View original profile</a>
|
<a href="{{ user.profile_id() }}" class="btn btn-primary">View original profile</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="" class="btn btn-warning confirm_first">Ban</a>
|
|
||||||
<a href="" class="btn btn-warning confirm_first">Ban + Purge</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
{% if user.created_recently() %}
|
{% if user.created_recently() %}
|
||||||
<span class="fe fe-new-account" title="New account"> </span>
|
<span class="fe fe-new-account" title="New account"> </span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.bot %}
|
||||||
|
<span class="fe fe-bot-account" title="Bot account"> </span>
|
||||||
|
{% endif %}
|
||||||
{% if user.reputation < -10 %}
|
{% 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>
|
||||||
<span class="fe fe-warning red" title="Very low reputation. Beware!"> </span>
|
<span class="fe fe-warning red" title="Very low reputation. Beware!"> </span>
|
||||||
|
|
|
@ -48,11 +48,12 @@
|
||||||
<td>{{ user.reports if user.reports > 0 }} </td>
|
<td>{{ user.reports if user.reports > 0 }} </td>
|
||||||
<td>{{ user.ip_address if user.ip_address }} </td>
|
<td>{{ user.ip_address if user.ip_address }} </td>
|
||||||
<td>{% if user.is_local() %}
|
<td>{% if user.is_local() %}
|
||||||
<a href="/u/{{ user.link() }}">View local</a>
|
<a href="/u/{{ user.link() }}">View</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<a href="/u/{{ user.link() }}">View local</a> |
|
||||||
<a href="{{ user.ap_profile_id }}">View remote</a>
|
<a href="{{ user.ap_profile_id }}">View remote</a>
|
||||||
{% endif %}
|
{% 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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
{% if comment['comment'].author.created_recently() %}
|
{% if comment['comment'].author.created_recently() %}
|
||||||
<span class="fe fe-new-account small" title="New account"> </span>
|
<span class="fe fe-new-account small" title="New account"> </span>
|
||||||
{% endif %}
|
{% 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.id != current_user.id %}
|
||||||
{% if comment['comment'].author.reputation < -10 %}
|
{% if comment['comment'].author.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>
|
||||||
|
|
|
@ -90,6 +90,9 @@
|
||||||
{% if comment['comment'].author.created_recently() %}
|
{% if comment['comment'].author.created_recently() %}
|
||||||
<span class="fe fe-new-account small" title="New account"> </span>
|
<span class="fe fe-new-account small" title="New account"> </span>
|
||||||
{% endif %}
|
{% 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.id != current_user.id %}
|
||||||
{% if comment['comment'].author.reputation < -10 %}
|
{% if comment['comment'].author.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>
|
||||||
|
|
|
@ -57,6 +57,9 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="small">{{ _('Joined') }}: {{ moment(user.created).fromNow(refresh=True) }}<br />
|
<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>
|
{{ _('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 }}
|
{{ user.about_html|safe }}
|
||||||
{% if posts %}
|
{% if posts %}
|
||||||
|
|
|
@ -16,7 +16,8 @@ from app.topic import bp
|
||||||
from app import db, celery, cache
|
from app import db, celery, cache
|
||||||
from app.topic.forms import ChooseTopicsForm
|
from app.topic.forms import ChooseTopicsForm
|
||||||
from app.utils import render_template, user_filters_posts, moderating_communities, joined_communities, \
|
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'])
|
@bp.route('/topic/<path:topic_path>', methods=['GET'])
|
||||||
|
@ -68,6 +69,9 @@ def show_topic(topic_path):
|
||||||
instance_ids = blocked_instances(current_user.id)
|
instance_ids = blocked_instances(current_user.id)
|
||||||
if instance_ids:
|
if instance_ids:
|
||||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
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
|
# sorting
|
||||||
if sort == '' or sort == 'hot':
|
if sort == '' or sort == 'hot':
|
||||||
|
|
|
@ -79,7 +79,7 @@ def show_profile(user):
|
||||||
description=description, subscribed=subscribed, upvoted=upvoted,
|
description=description, subscribed=subscribed, upvoted=upvoted,
|
||||||
post_next_url=post_next_url, post_prev_url=post_prev_url,
|
post_next_url=post_next_url, post_prev_url=post_prev_url,
|
||||||
replies_next_url=replies_next_url, replies_prev_url=replies_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()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id())
|
joined_communities=joined_communities(current_user.get_id())
|
||||||
)
|
)
|
||||||
|
|
|
@ -149,7 +149,7 @@ def search_for_user(address: str):
|
||||||
if user_data.status_code == 200:
|
if user_data.status_code == 200:
|
||||||
user_json = user_data.json()
|
user_json = user_data.json()
|
||||||
user_data.close()
|
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)
|
user = actor_json_to_model(user_json, name, server)
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
47
app/utils.py
47
app/utils.py
|
@ -28,7 +28,7 @@ import re
|
||||||
|
|
||||||
from app.email import send_welcome_email
|
from app.email import send_welcome_email
|
||||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
|
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
|
# 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:
|
def markdown_to_html(markdown_text) -> str:
|
||||||
if markdown_text:
|
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:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -312,6 +312,12 @@ def community_membership(user: User, community: Community) -> int:
|
||||||
return user.subscribed(community.id)
|
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)
|
@cache.memoize(timeout=86400)
|
||||||
def blocked_domains(user_id) -> List[int]:
|
def blocked_domains(user_id) -> List[int]:
|
||||||
blocks = DomainBlock.query.filter_by(user_id=user_id)
|
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]
|
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():
|
def retrieve_block_list():
|
||||||
try:
|
try:
|
||||||
response = requests.get('https://raw.githubusercontent.com/rimu/no-qanon/master/domains.txt', timeout=1)
|
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:
|
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
|
return False
|
||||||
|
|
||||||
if site is None:
|
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.
|
if user.attitude < -0.40 or user.reputation < -10: # this should exclude about 3.7% of users.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if community.id in communities_banned_from(user.id):
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def can_upvote(user, community: Community) -> bool:
|
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 False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -483,6 +512,9 @@ def can_create_post(user, content: Community) -> bool:
|
||||||
if content.local_only and not user.is_local():
|
if content.local_only and not user.is_local():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if content.id in communities_banned_from(user.id):
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -496,6 +528,9 @@ def can_create_post_reply(user, content: Community) -> bool:
|
||||||
if content.local_only and not user.is_local():
|
if content.local_only and not user.is_local():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if content.id in communities_banned_from(user.id):
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -602,7 +637,8 @@ def moderating_communities(user_id):
|
||||||
return []
|
return []
|
||||||
return Community.query.join(CommunityMember, Community.id == CommunityMember.community_id).\
|
return Community.query.join(CommunityMember, Community.id == CommunityMember.community_id).\
|
||||||
filter(Community.banned == False).\
|
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()
|
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).\
|
return Community.query.join(CommunityMember, Community.id == CommunityMember.community_id).\
|
||||||
filter(Community.banned == False). \
|
filter(Community.banned == False). \
|
||||||
filter(CommunityMember.is_moderator == False, CommunityMember.is_owner == 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()
|
filter(CommunityMember.user_id == user_id).order_by(Community.title).all()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,3 +42,10 @@ class Config(object):
|
||||||
SENTRY_DSN = os.environ.get('SENTRY_DSN') or None
|
SENTRY_DSN = os.environ.get('SENTRY_DSN') or None
|
||||||
|
|
||||||
AWS_REGION = os.environ.get('AWS_REGION') 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 ''
|
||||||
|
|
|
@ -26,3 +26,6 @@ FLASK_APP = 'pyfedi.py'
|
||||||
SENTRY_DSN = ''
|
SENTRY_DSN = ''
|
||||||
|
|
||||||
AWS_REGION = 'ap-southeast-2'
|
AWS_REGION = 'ap-southeast-2'
|
||||||
|
|
||||||
|
CLOUDFLARE_API_TOKEN = ''
|
||||||
|
CLOUDFLARE_ZONE_ID = ''
|
32
migrations/versions/2b028a70bd7a_blocked_phrases.py
Normal file
32
migrations/versions/2b028a70bd7a_blocked_phrases.py
Normal 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 ###
|
|
@ -30,3 +30,4 @@ redis==5.0.1
|
||||||
Werkzeug==2.3.3
|
Werkzeug==2.3.3
|
||||||
pytesseract==0.3.10
|
pytesseract==0.3.10
|
||||||
sentry-sdk==1.40.6
|
sentry-sdk==1.40.6
|
||||||
|
urllib3==1.26.1
|
||||||
|
|
Loading…
Add table
Reference in a new issue