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
f2ceb5752d
31 changed files with 577 additions and 190 deletions
|
@ -18,9 +18,9 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
|
||||||
lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \
|
lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \
|
||||||
upvote_post, activity_already_ingested, delete_post_or_comment, community_members, \
|
upvote_post, activity_already_ingested, delete_post_or_comment, community_members, \
|
||||||
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
|
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
|
||||||
update_post_from_activity, undo_vote, undo_downvote
|
update_post_from_activity, undo_vote, undo_downvote, post_to_page
|
||||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
|
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \
|
||||||
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \
|
domain_from_url, markdown_to_html, community_membership, ap_datetime, ip_address, can_downvote, \
|
||||||
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \
|
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \
|
||||||
community_moderators
|
community_moderators
|
||||||
import werkzeug.exceptions
|
import werkzeug.exceptions
|
||||||
|
@ -175,12 +175,12 @@ def user_profile(actor):
|
||||||
actor = actor.strip()
|
actor = actor.strip()
|
||||||
if current_user.is_authenticated and current_user.is_admin():
|
if current_user.is_authenticated and current_user.is_admin():
|
||||||
if '@' in actor:
|
if '@' in actor:
|
||||||
user: User = User.query.filter_by(ap_id=actor).first()
|
user: User = User.query.filter_by(ap_id=actor.lower()).first()
|
||||||
else:
|
else:
|
||||||
user: User = User.query.filter_by(user_name=actor, ap_id=None).first()
|
user: User = User.query.filter_by(user_name=actor, ap_id=None).first()
|
||||||
else:
|
else:
|
||||||
if '@' in actor:
|
if '@' in actor:
|
||||||
user: User = User.query.filter_by(ap_id=actor, deleted=False, banned=False).first()
|
user: User = User.query.filter_by(ap_id=actor.lower(), deleted=False, banned=False).first()
|
||||||
else:
|
else:
|
||||||
user: User = User.query.filter_by(user_name=actor, deleted=False, ap_id=None).first()
|
user: User = User.query.filter_by(user_name=actor, deleted=False, ap_id=None).first()
|
||||||
|
|
||||||
|
@ -419,6 +419,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
||||||
body_html=allowlist_html(markdown_to_html(request_json['object']['source']['content'])),
|
body_html=allowlist_html(markdown_to_html(request_json['object']['source']['content'])),
|
||||||
encrypted=encrypted)
|
encrypted=encrypted)
|
||||||
db.session.add(new_message)
|
db.session.add(new_message)
|
||||||
|
existing_conversation.updated_at = utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Notify recipient
|
# Notify recipient
|
||||||
|
@ -432,20 +433,43 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
||||||
activity_log.result = 'success'
|
activity_log.result = 'success'
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
community_ap_id = request_json['to'][0]
|
community_ap_id = ''
|
||||||
if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public': # kbin does this when posting a reply
|
locations = ['audience', 'cc', 'to']
|
||||||
if 'to' in request_json['object'] and request_json['object']['to']:
|
if 'object' in request_json:
|
||||||
community_ap_id = request_json['object']['to'][0]
|
rjs = [ request_json, request_json['object'] ]
|
||||||
if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public' and 'cc' in \
|
else:
|
||||||
request_json['object'] and request_json['object']['cc']:
|
rjs = [ request_json ]
|
||||||
community_ap_id = request_json['object']['cc'][0]
|
local_community_prefix = f"https://{current_app.config['SERVER_NAME']}/c/"
|
||||||
elif 'cc' in request_json['object'] and request_json['object']['cc']:
|
followers_suffix = '/followers'
|
||||||
community_ap_id = request_json['object']['cc'][0]
|
for rj in rjs:
|
||||||
if community_ap_id.endswith('/followers'): # mastodon
|
for loc in locations:
|
||||||
if 'inReplyTo' in request_json['object']:
|
if loc in rj:
|
||||||
post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
|
id = rj[loc]
|
||||||
if post_being_replied_to:
|
if isinstance(id, str):
|
||||||
community_ap_id = post_being_replied_to.community.ap_profile_id
|
if id.startswith(local_community_prefix) and not id.endswith(followers_suffix):
|
||||||
|
community_ap_id = id
|
||||||
|
if isinstance(id, list):
|
||||||
|
for c in id:
|
||||||
|
if c.startswith(local_community_prefix) and not c.endswith(followers_suffix):
|
||||||
|
community_ap_id = c
|
||||||
|
break
|
||||||
|
if community_ap_id:
|
||||||
|
break
|
||||||
|
if community_ap_id:
|
||||||
|
break
|
||||||
|
if not community_ap_id and 'object' in request_json and 'inReplyTo' in request_json['object']:
|
||||||
|
post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
|
||||||
|
if post_being_replied_to:
|
||||||
|
community_ap_id = post_being_replied_to.community.ap_profile_id
|
||||||
|
else:
|
||||||
|
comment_being_replied_to = PostReply.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
|
||||||
|
if comment_being_replied_to:
|
||||||
|
community_ap_id = comment_being_replied_to.community.ap_profile_id
|
||||||
|
if not community_ap_id:
|
||||||
|
activity_log.result = 'failure'
|
||||||
|
activity_log.exception_message = 'Unable to extract community'
|
||||||
|
db.session.commit()
|
||||||
|
return
|
||||||
except:
|
except:
|
||||||
activity_log.activity_type = 'exception'
|
activity_log.activity_type = 'exception'
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -1029,6 +1053,27 @@ def community_outbox(actor):
|
||||||
return jsonify(community_data)
|
return jsonify(community_data)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/c/<actor>/featured', methods=['GET'])
|
||||||
|
def community_featured(actor):
|
||||||
|
actor = actor.strip()
|
||||||
|
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||||
|
if community is not None:
|
||||||
|
posts = Post.query.filter_by(community_id=community.id, sticky=True).all()
|
||||||
|
|
||||||
|
community_data = {
|
||||||
|
"@context": default_context(),
|
||||||
|
"type": "OrderedCollection",
|
||||||
|
"id": f"https://{current_app.config['SERVER_NAME']}/c/{actor}/featured",
|
||||||
|
"totalItems": len(posts),
|
||||||
|
"orderedItems": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
community_data['orderedItems'].append(post_to_page(post, community))
|
||||||
|
|
||||||
|
return jsonify(community_data)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/c/<actor>/moderators', methods=['GET'])
|
@bp.route('/c/<actor>/moderators', methods=['GET'])
|
||||||
def community_moderators_route(actor):
|
def community_moderators_route(actor):
|
||||||
actor = actor.strip()
|
actor = actor.strip()
|
||||||
|
@ -1069,7 +1114,7 @@ def community_followers(actor):
|
||||||
if community is not None:
|
if community is not None:
|
||||||
result = {
|
result = {
|
||||||
"@context": default_context(),
|
"@context": default_context(),
|
||||||
"id": f'https://{current_app.config["SERVER_NAME"]}/c/actor/followers',
|
"id": f'https://{current_app.config["SERVER_NAME"]}/c/{actor}/followers',
|
||||||
"type": "Collection",
|
"type": "Collection",
|
||||||
"totalItems": community_members(community.id),
|
"totalItems": community_members(community.id),
|
||||||
"items": []
|
"items": []
|
||||||
|
|
|
@ -22,10 +22,10 @@ from PIL import Image, ImageOps
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import pytesseract
|
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, 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
|
blocked_phrases, microblog_content_to_title
|
||||||
|
|
||||||
|
|
||||||
def public_key():
|
def public_key():
|
||||||
|
@ -178,6 +178,43 @@ def post_to_activity(post: Post, community: Community):
|
||||||
return activity_data
|
return activity_data
|
||||||
|
|
||||||
|
|
||||||
|
def post_to_page(post: Post, community: Community):
|
||||||
|
activity_data = {
|
||||||
|
"type": "Page",
|
||||||
|
"id": post.ap_id,
|
||||||
|
"attributedTo": post.author.ap_public_url,
|
||||||
|
"to": [
|
||||||
|
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}",
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
],
|
||||||
|
"name": post.title,
|
||||||
|
"cc": [],
|
||||||
|
"content": post.body_html if post.body_html else '',
|
||||||
|
"mediaType": "text/html",
|
||||||
|
"source": {
|
||||||
|
"content": post.body if post.body else '',
|
||||||
|
"mediaType": "text/markdown"
|
||||||
|
},
|
||||||
|
"attachment": [],
|
||||||
|
"commentsEnabled": post.comments_enabled,
|
||||||
|
"sensitive": post.nsfw or post.nsfl,
|
||||||
|
"published": ap_datetime(post.created_at),
|
||||||
|
"stickied": post.sticky,
|
||||||
|
"audience": f"https://{current_app.config['SERVER_NAME']}/c/{community.name}"
|
||||||
|
}
|
||||||
|
if post.edited_at is not None:
|
||||||
|
activity_data["updated"] = ap_datetime(post.edited_at)
|
||||||
|
if post.language is not None:
|
||||||
|
activity_data["language"] = {"identifier": post.language}
|
||||||
|
if post.type == POST_TYPE_LINK and post.url is not None:
|
||||||
|
activity_data["attachment"] = [{"href": post.url, "type": "Link"}]
|
||||||
|
if post.image_id is not None:
|
||||||
|
activity_data["image"] = {"url": post.image.view_url(), "type": "Image"}
|
||||||
|
if post.image.alt_text:
|
||||||
|
activity_data["image"]['altText'] = post.image.alt_text
|
||||||
|
return activity_data
|
||||||
|
|
||||||
|
|
||||||
def banned_user_agents():
|
def banned_user_agents():
|
||||||
return [] # todo: finish this function
|
return [] # todo: finish this function
|
||||||
|
|
||||||
|
@ -227,6 +264,11 @@ def find_actor_or_create(actor: str, create_if_not_found=True, community_only=Fa
|
||||||
return None
|
return None
|
||||||
if user is None:
|
if user is None:
|
||||||
user = Community.query.filter(Community.ap_profile_id == actor).first()
|
user = Community.query.filter(Community.ap_profile_id == actor).first()
|
||||||
|
if user and user.banned:
|
||||||
|
# Try to find a non-banned copy of the community. Sometimes duplicates happen and one copy is banned.
|
||||||
|
user = Community.query.filter(Community.ap_profile_id == actor).filter(Community.banned == False).first()
|
||||||
|
if user is None: # no un-banned version of this community exists, only the banned one. So it was banned for being bad, not for being a duplicate.
|
||||||
|
return None
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
if not user.is_local() and (user.ap_fetched_at is None or user.ap_fetched_at < utcnow() - timedelta(days=7)):
|
if not user.is_local() and (user.ap_fetched_at is None or user.ap_fetched_at < utcnow() - timedelta(days=7)):
|
||||||
|
@ -429,7 +471,7 @@ def refresh_community_profile_task(community_id):
|
||||||
community.description_html = markdown_to_html(community.description)
|
community.description_html = markdown_to_html(community.description)
|
||||||
elif 'content' in activity_json:
|
elif 'content' in activity_json:
|
||||||
community.description_html = allowlist_html(activity_json['content'])
|
community.description_html = allowlist_html(activity_json['content'])
|
||||||
community.description = html_to_markdown(community.description_html)
|
community.description = ''
|
||||||
|
|
||||||
icon_changed = cover_changed = False
|
icon_changed = cover_changed = False
|
||||||
if 'icon' in activity_json:
|
if 'icon' in activity_json:
|
||||||
|
@ -564,7 +606,7 @@ def actor_json_to_model(activity_json, address, server):
|
||||||
ap_followers_url=activity_json['followers'],
|
ap_followers_url=activity_json['followers'],
|
||||||
ap_inbox_url=activity_json['endpoints']['sharedInbox'],
|
ap_inbox_url=activity_json['endpoints']['sharedInbox'],
|
||||||
ap_outbox_url=activity_json['outbox'],
|
ap_outbox_url=activity_json['outbox'],
|
||||||
ap_featured_url=activity_json['featured'],
|
ap_featured_url=activity_json['featured'] if 'featured' in activity_json else '',
|
||||||
ap_moderators_url=mods_url,
|
ap_moderators_url=mods_url,
|
||||||
ap_fetched_at=utcnow(),
|
ap_fetched_at=utcnow(),
|
||||||
ap_domain=server,
|
ap_domain=server,
|
||||||
|
@ -580,7 +622,7 @@ def actor_json_to_model(activity_json, address, server):
|
||||||
community.description_html = markdown_to_html(community.description)
|
community.description_html = markdown_to_html(community.description)
|
||||||
elif 'content' in activity_json:
|
elif 'content' in activity_json:
|
||||||
community.description_html = allowlist_html(activity_json['content'])
|
community.description_html = allowlist_html(activity_json['content'])
|
||||||
community.description = html_to_markdown(community.description_html)
|
community.description = ''
|
||||||
if 'icon' in activity_json:
|
if 'icon' in activity_json:
|
||||||
icon = File(source_url=activity_json['icon']['url'])
|
icon = File(source_url=activity_json['icon']['url'])
|
||||||
community.icon = icon
|
community.icon = icon
|
||||||
|
@ -620,7 +662,7 @@ def post_json_to_model(activity_log, post_json, user, community) -> Post:
|
||||||
post.body_html = markdown_to_html(post.body)
|
post.body_html = markdown_to_html(post.body)
|
||||||
elif 'content' in post_json:
|
elif 'content' in post_json:
|
||||||
post.body_html = allowlist_html(post_json['content'])
|
post.body_html = allowlist_html(post_json['content'])
|
||||||
post.body = html_to_markdown(post.body_html)
|
post.body = ''
|
||||||
if 'attachment' in post_json and len(post_json['attachment']) > 0 and 'type' in post_json['attachment'][0]:
|
if 'attachment' in post_json and len(post_json['attachment']) > 0 and 'type' in post_json['attachment'][0]:
|
||||||
if post_json['attachment'][0]['type'] == 'Link':
|
if post_json['attachment'][0]['type'] == 'Link':
|
||||||
post.url = post_json['attachment'][0]['href']
|
post.url = post_json['attachment'][0]['href']
|
||||||
|
@ -768,7 +810,7 @@ def parse_summary(user_json) -> str:
|
||||||
if 'source' in user_json and user_json['source'].get('mediaType') == 'text/markdown':
|
if 'source' in user_json and user_json['source'].get('mediaType') == 'text/markdown':
|
||||||
# Convert Markdown to HTML
|
# Convert Markdown to HTML
|
||||||
markdown_text = user_json['source']['content']
|
markdown_text = user_json['source']['content']
|
||||||
html_content = html_to_markdown(markdown_text)
|
html_content = allowlist_html(markdown_to_html(markdown_text))
|
||||||
return html_content
|
return html_content
|
||||||
elif 'summary' in user_json:
|
elif 'summary' in user_json:
|
||||||
return allowlist_html(user_json['summary'])
|
return allowlist_html(user_json['summary'])
|
||||||
|
@ -1179,7 +1221,7 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
|
||||||
post_reply.body_html = markdown_to_html(post_reply.body)
|
post_reply.body_html = markdown_to_html(post_reply.body)
|
||||||
elif 'content' in request_json['object']: # Kbin
|
elif 'content' in request_json['object']: # Kbin
|
||||||
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 = ''
|
||||||
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.
|
# Discard post_reply if it contains certain phrases. Good for stopping spam floods.
|
||||||
if post_reply.body:
|
if post_reply.body:
|
||||||
|
@ -1253,11 +1295,17 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
||||||
activity_log.exception_message = 'Community is local only, post discarded'
|
activity_log.exception_message = 'Community is local only, post discarded'
|
||||||
activity_log.result = 'ignored'
|
activity_log.result = 'ignored'
|
||||||
return None
|
return None
|
||||||
if 'name' not in request_json['object']: # Microblog posts sometimes get Announced by lemmy. They don't have a title, so we can't use them.
|
if 'name' not in request_json['object']: # Microblog posts
|
||||||
return None
|
if 'content' in request_json['object'] and request_json['object']['content'] is not None:
|
||||||
nsfl_in_title = '[NSFL]' in request_json['object']['name'].upper() or '(NSFL)' in request_json['object']['name'].upper()
|
name = "[Microblog]"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
name = request_json['object']['name']
|
||||||
|
|
||||||
|
nsfl_in_title = '[NSFL]' in name.upper() or '(NSFL)' in name.upper()
|
||||||
post = Post(user_id=user.id, community_id=community.id,
|
post = Post(user_id=user.id, community_id=community.id,
|
||||||
title=html.unescape(request_json['object']['name']),
|
title=html.unescape(name),
|
||||||
comments_enabled=request_json['object']['commentsEnabled'] if 'commentsEnabled' in request_json['object'] else True,
|
comments_enabled=request_json['object']['commentsEnabled'] if 'commentsEnabled' in request_json['object'] else True,
|
||||||
sticky=request_json['object']['stickied'] if 'stickied' in request_json['object'] else False,
|
sticky=request_json['object']['stickied'] if 'stickied' in request_json['object'] else False,
|
||||||
nsfw=request_json['object']['sensitive'] if 'sensitive' in request_json['object'] else False,
|
nsfw=request_json['object']['sensitive'] if 'sensitive' in request_json['object'] else False,
|
||||||
|
@ -1278,7 +1326,12 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
||||||
post.body_html = markdown_to_html(post.body)
|
post.body_html = markdown_to_html(post.body)
|
||||||
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 = ''
|
||||||
|
if name == "[Microblog]":
|
||||||
|
name += ' ' + microblog_content_to_title(post.body_html)
|
||||||
|
if '[NSFL]' in name.upper() or '(NSFL)' in name.upper():
|
||||||
|
post.nsfl = True
|
||||||
|
post.title = name
|
||||||
# Discard post if it contains certain phrases. Good for stopping spam floods.
|
# Discard post if it contains certain phrases. Good for stopping spam floods.
|
||||||
blocked_phrases_list = blocked_phrases()
|
blocked_phrases_list = blocked_phrases()
|
||||||
for blocked_phrase in blocked_phrases_list:
|
for blocked_phrase in blocked_phrases_list:
|
||||||
|
@ -1291,7 +1344,10 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
||||||
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':
|
||||||
post.url = request_json['object']['attachment'][0]['href']
|
post.url = request_json['object']['attachment'][0]['href'] # Lemmy
|
||||||
|
if request_json['object']['attachment'][0]['type'] == 'Document':
|
||||||
|
post.url = request_json['object']['attachment'][0]['url'] # Mastodon
|
||||||
|
if post.url:
|
||||||
if is_image_url(post.url):
|
if is_image_url(post.url):
|
||||||
post.type = POST_TYPE_IMAGE
|
post.type = POST_TYPE_IMAGE
|
||||||
if 'image' in request_json['object'] and 'url' in request_json['object']['image']:
|
if 'image' in request_json['object'] and 'url' in request_json['object']['image']:
|
||||||
|
@ -1370,7 +1426,7 @@ def update_post_reply_from_activity(reply: PostReply, request_json: dict):
|
||||||
reply.body_html = markdown_to_html(reply.body)
|
reply.body_html = markdown_to_html(reply.body)
|
||||||
elif 'content' in request_json['object']:
|
elif 'content' in request_json['object']:
|
||||||
reply.body_html = allowlist_html(request_json['object']['content'])
|
reply.body_html = allowlist_html(request_json['object']['content'])
|
||||||
reply.body = html_to_markdown(reply.body_html)
|
reply.body = ''
|
||||||
reply.edited_at = utcnow()
|
reply.edited_at = utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
@ -1384,7 +1440,7 @@ def update_post_from_activity(post: Post, request_json: dict):
|
||||||
post.body_html = markdown_to_html(post.body)
|
post.body_html = markdown_to_html(post.body)
|
||||||
elif 'content' in request_json['object']:
|
elif 'content' in request_json['object']:
|
||||||
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 = ''
|
||||||
if 'attachment' in request_json['object'] and 'href' in request_json['object']['attachment']:
|
if 'attachment' in request_json['object'] and 'href' in request_json['object']['attachment']:
|
||||||
post.url = request_json['object']['attachment']['href']
|
post.url = request_json['object']['attachment']['href']
|
||||||
if 'sensitive' in request_json['object']:
|
if 'sensitive' in request_json['object']:
|
||||||
|
|
|
@ -4,7 +4,7 @@ from time import sleep
|
||||||
from flask import request, flash, json, url_for, current_app, redirect, g
|
from flask import request, flash, json, url_for, current_app, redirect, g
|
||||||
from flask_login import login_required, current_user
|
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, or_
|
||||||
|
|
||||||
from app import db, celery, cache
|
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
|
||||||
|
@ -15,6 +15,7 @@ from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditC
|
||||||
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, \
|
||||||
topic_tree, topics_for_form
|
topic_tree, topics_for_form
|
||||||
from app.community.util import save_icon_file, save_banner_file
|
from app.community.util import save_icon_file, save_banner_file
|
||||||
|
from app.constants import REPORT_STATE_NEW, REPORT_STATE_ESCALATED
|
||||||
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, \
|
||||||
|
@ -627,7 +628,7 @@ def admin_users_add():
|
||||||
private_key, public_key = RsaKeys.generate_keypair()
|
private_key, public_key = RsaKeys.generate_keypair()
|
||||||
user.private_key = private_key
|
user.private_key = private_key
|
||||||
user.public_key = public_key
|
user.public_key = public_key
|
||||||
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}".lower()
|
||||||
user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
||||||
user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
|
user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
|
||||||
user.roles.append(Role.query.get(form.role.data))
|
user.roles.append(Role.query.get(form.role.data))
|
||||||
|
@ -674,7 +675,7 @@ def admin_reports():
|
||||||
search = request.args.get('search', '')
|
search = request.args.get('search', '')
|
||||||
local_remote = request.args.get('local_remote', '')
|
local_remote = request.args.get('local_remote', '')
|
||||||
|
|
||||||
reports = Report.query.filter_by(status=0)
|
reports = Report.query.filter(or_(Report.status == REPORT_STATE_NEW, Report.status == REPORT_STATE_ESCALATED))
|
||||||
if local_remote == 'local':
|
if local_remote == 'local':
|
||||||
reports = reports.filter_by(ap_id=None)
|
reports = reports.filter_by(ap_id=None)
|
||||||
if local_remote == 'remote':
|
if local_remote == 'remote':
|
||||||
|
|
|
@ -99,7 +99,7 @@ def register():
|
||||||
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():
|
for referrer in blocked_referrers():
|
||||||
if referrer in session.get('Referer'):
|
if referrer in session.get('Referer', ''):
|
||||||
resp = make_response(redirect(url_for('auth.please_wait')))
|
resp = make_response(redirect(url_for('auth.please_wait')))
|
||||||
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
||||||
return resp
|
return resp
|
||||||
|
@ -112,7 +112,7 @@ def register():
|
||||||
user = User(user_name=form.user_name.data, title=form.user_name.data, email=form.real_email.data,
|
user = User(user_name=form.user_name.data, title=form.user_name.data, email=form.real_email.data,
|
||||||
verification_token=verification_token, instance_id=1, ip_address=ip_address(),
|
verification_token=verification_token, instance_id=1, ip_address=ip_address(),
|
||||||
banned=user_ip_banned() or user_cookie_banned(), email_unread_sent=False,
|
banned=user_ip_banned() or user_cookie_banned(), email_unread_sent=False,
|
||||||
referrer=session.get('Referer'))
|
referrer=session.get('Referer', ''))
|
||||||
user.set_password(form.password.data)
|
user.set_password(form.password.data)
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
|
@ -145,7 +145,7 @@ def chat_report(conversation_id):
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||||
type=4, reporter_id=current_user.id, suspect_conversation_id=conversation_id)
|
type=4, reporter_id=current_user.id, suspect_conversation_id=conversation_id, source_instance_id=1)
|
||||||
db.session.add(report)
|
db.session.add(report)
|
||||||
|
|
||||||
# Notify site admin
|
# Notify site admin
|
||||||
|
|
|
@ -13,6 +13,7 @@ def send_message(message: str, conversation_id: int) -> ChatMessage:
|
||||||
conversation = Conversation.query.get(conversation_id)
|
conversation = Conversation.query.get(conversation_id)
|
||||||
reply = ChatMessage(sender_id=current_user.id, conversation_id=conversation.id,
|
reply = ChatMessage(sender_id=current_user.id, conversation_id=conversation.id,
|
||||||
body=message, body_html=allowlist_html(markdown_to_html(message)))
|
body=message, body_html=allowlist_html(markdown_to_html(message)))
|
||||||
|
conversation.updated_at = utcnow()
|
||||||
for recipient in conversation.members:
|
for recipient in conversation.members:
|
||||||
if recipient.id != current_user.id:
|
if recipient.id != current_user.id:
|
||||||
if recipient.is_local():
|
if recipient.is_local():
|
||||||
|
|
|
@ -3,7 +3,7 @@ from flask_login import current_user
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField, \
|
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField, \
|
||||||
DateField
|
DateField
|
||||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Regexp, Optional
|
||||||
from flask_babel import _, lazy_gettext as _l
|
from flask_babel import _, lazy_gettext as _l
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
@ -61,6 +61,17 @@ class AddModeratorForm(FlaskForm):
|
||||||
submit = SubmitField(_l('Add'))
|
submit = SubmitField(_l('Add'))
|
||||||
|
|
||||||
|
|
||||||
|
class EscalateReportForm(FlaskForm):
|
||||||
|
reason = StringField(_l('Amend the report description if necessary'), validators=[DataRequired()])
|
||||||
|
submit = SubmitField(_l('Escalate report'))
|
||||||
|
|
||||||
|
|
||||||
|
class ResolveReportForm(FlaskForm):
|
||||||
|
note = StringField(_l('Note for mod log'), validators=[Optional()])
|
||||||
|
also_resolve_others = BooleanField(_l('Also resolve all other reports about the same thing.'), default=True)
|
||||||
|
submit = SubmitField(_l('Resolve report'))
|
||||||
|
|
||||||
|
|
||||||
class SearchRemoteCommunity(FlaskForm):
|
class SearchRemoteCommunity(FlaskForm):
|
||||||
address = StringField(_l('Community address'), render_kw={'placeholder': 'e.g. !name@server', 'autofocus': True}, validators=[DataRequired()])
|
address = StringField(_l('Community address'), render_kw={'placeholder': 'e.g. !name@server', 'autofocus': True}, validators=[DataRequired()])
|
||||||
submit = SubmitField(_l('Search'))
|
submit = SubmitField(_l('Search'))
|
||||||
|
@ -77,15 +88,15 @@ class BanUserCommunityForm(FlaskForm):
|
||||||
class CreatePostForm(FlaskForm):
|
class CreatePostForm(FlaskForm):
|
||||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||||
post_type = HiddenField() # https://getbootstrap.com/docs/4.6/components/navs/#tabs
|
post_type = HiddenField() # https://getbootstrap.com/docs/4.6/components/navs/#tabs
|
||||||
discussion_title = StringField(_l('Title'), validators={Optional(), Length(min=3, max=255)})
|
discussion_title = StringField(_l('Title'), validators=[Optional(), Length(min=3, max=255)])
|
||||||
discussion_body = TextAreaField(_l('Body'), validators={Optional(), Length(min=3, max=5000)}, render_kw={'placeholder': 'Text (optional)'})
|
discussion_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'placeholder': 'Text (optional)'})
|
||||||
link_title = StringField(_l('Title'), validators={Optional(), Length(min=3, max=255)})
|
link_title = StringField(_l('Title'), validators=[Optional(), Length(min=3, max=255)])
|
||||||
link_body = TextAreaField(_l('Body'), validators={Optional(), Length(min=3, max=5000)},
|
link_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)],
|
||||||
render_kw={'placeholder': 'Text (optional)'})
|
render_kw={'placeholder': 'Text (optional)'})
|
||||||
link_url = StringField(_l('URL'), render_kw={'placeholder': 'https://...'})
|
link_url = StringField(_l('URL'), validators=[Optional(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')], render_kw={'placeholder': 'https://...'})
|
||||||
image_title = StringField(_l('Title'), validators={Optional(), Length(min=3, max=255)})
|
image_title = StringField(_l('Title'), validators=[Optional(), Length(min=3, max=255)])
|
||||||
image_alt_text = StringField(_l('Alt text'), validators={Optional(), Length(min=3, max=255)})
|
image_alt_text = StringField(_l('Alt text'), validators=[Optional(), Length(min=3, max=255)])
|
||||||
image_body = TextAreaField(_l('Body'), validators={Optional(), Length(min=3, max=5000)},
|
image_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)],
|
||||||
render_kw={'placeholder': 'Text (optional)'})
|
render_kw={'placeholder': 'Text (optional)'})
|
||||||
image_file = FileField(_('Image'))
|
image_file = FileField(_('Image'))
|
||||||
# flair = SelectField(_l('Flair'), coerce=int)
|
# flair = SelectField(_l('Flair'), coerce=int)
|
||||||
|
|
|
@ -5,19 +5,20 @@ from random import randint
|
||||||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort, g, json
|
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort, g, json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from sqlalchemy import or_, desc
|
from sqlalchemy import or_, desc, text
|
||||||
|
|
||||||
from app import db, constants, cache
|
from app import db, constants, cache
|
||||||
from app.activitypub.signature import RsaKeys, post_request
|
from app.activitypub.signature import RsaKeys, post_request
|
||||||
from app.activitypub.util import default_context, notify_about_post, find_actor_or_create, make_image_sizes
|
from app.activitypub.util import default_context, notify_about_post, find_actor_or_create, make_image_sizes
|
||||||
from app.chat.util import send_message
|
from app.chat.util import send_message
|
||||||
from app.community.forms import SearchRemoteCommunity, CreatePostForm, ReportCommunityForm, \
|
from app.community.forms import SearchRemoteCommunity, CreatePostForm, ReportCommunityForm, \
|
||||||
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm
|
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \
|
||||||
|
EscalateReportForm, ResolveReportForm
|
||||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
||||||
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
||||||
delete_post_from_community, delete_post_reply_from_community
|
delete_post_from_community, delete_post_reply_from_community
|
||||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
|
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
|
||||||
SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR
|
SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR, REPORT_STATE_NEW, REPORT_STATE_ESCALATED, REPORT_STATE_RESOLVED
|
||||||
from app.inoculation import inoculation
|
from app.inoculation import inoculation
|
||||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||||
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply
|
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply
|
||||||
|
@ -49,7 +50,8 @@ def add_local():
|
||||||
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
||||||
public_key=public_key, description_html=markdown_to_html(form.description.data),
|
public_key=public_key, description_html=markdown_to_html(form.description.data),
|
||||||
rules_html=markdown_to_html(form.rules.data), local_only=form.local_only.data,
|
rules_html=markdown_to_html(form.rules.data), local_only=form.local_only.data,
|
||||||
ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data.lower(),
|
||||||
|
ap_public_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
||||||
ap_followers_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data + '/followers',
|
ap_followers_url='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data + '/followers',
|
||||||
ap_domain=current_app.config['SERVER_NAME'],
|
ap_domain=current_app.config['SERVER_NAME'],
|
||||||
subscriptions_count=1, instance_id=1, low_quality='memes' in form.url.data)
|
subscriptions_count=1, instance_id=1, low_quality='memes' in form.url.data)
|
||||||
|
@ -102,9 +104,9 @@ def add_remote():
|
||||||
flash(_('Community not found.'), 'warning')
|
flash(_('Community not found.'), 'warning')
|
||||||
else:
|
else:
|
||||||
flash(_('Community not found. If you are searching for a nsfw community it is blocked by this instance.'), 'warning')
|
flash(_('Community not found. If you are searching for a nsfw community it is blocked by this instance.'), 'warning')
|
||||||
|
else:
|
||||||
if new_community.banned:
|
if new_community.banned:
|
||||||
flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning')
|
flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning')
|
||||||
|
|
||||||
return render_template('community/add_remote.html',
|
return render_template('community/add_remote.html',
|
||||||
title=_('Add remote community'), form=form, new_community=new_community,
|
title=_('Add remote community'), form=form, new_community=new_community,
|
||||||
|
@ -577,7 +579,7 @@ def community_report(community_id: int):
|
||||||
form = ReportCommunityForm()
|
form = ReportCommunityForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||||
type=1, reporter_id=current_user.id, suspect_community_id=community.id)
|
type=1, reporter_id=current_user.id, suspect_community_id=community.id, source_instance_id=1)
|
||||||
db.session.add(report)
|
db.session.add(report)
|
||||||
|
|
||||||
# Notify admin
|
# Notify admin
|
||||||
|
@ -638,7 +640,8 @@ def community_edit(community_id: int):
|
||||||
community.image = file
|
community.image = file
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
community.topic.num_communities = community.topic.communities.count()
|
if community.topic:
|
||||||
|
community.topic.num_communities = community.topic.communities.count()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Saved'))
|
flash(_('Saved'))
|
||||||
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
|
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
|
||||||
|
@ -653,7 +656,7 @@ def community_edit(community_id: int):
|
||||||
form.topic.data = community.topic_id if community.topic_id else None
|
form.topic.data = community.topic_id if community.topic_id else None
|
||||||
form.default_layout.data = community.default_layout
|
form.default_layout.data = community.default_layout
|
||||||
return render_template('community/community_edit.html', title=_('Edit community'), form=form,
|
return render_template('community/community_edit.html', title=_('Edit community'), form=form,
|
||||||
current_app=current_app,
|
current_app=current_app, current="edit_settings",
|
||||||
community=community, moderating_communities=moderating_communities(current_user.get_id()),
|
community=community, moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()))
|
joined_communities=joined_communities(current_user.get_id()))
|
||||||
else:
|
else:
|
||||||
|
@ -694,7 +697,7 @@ def community_mod_list(community_id: int):
|
||||||
filter(CommunityMember.community_id == community_id, or_(CommunityMember.is_moderator == True, CommunityMember.is_owner == True)).all()
|
filter(CommunityMember.community_id == community_id, or_(CommunityMember.is_moderator == True, CommunityMember.is_owner == True)).all()
|
||||||
|
|
||||||
return render_template('community/community_mod_list.html', title=_('Moderators for %(community)s', community=community.display_name()),
|
return render_template('community/community_mod_list.html', title=_('Moderators for %(community)s', community=community.display_name()),
|
||||||
moderators=moderators, community=community,
|
moderators=moderators, community=community, current="moderators",
|
||||||
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())
|
||||||
)
|
)
|
||||||
|
@ -923,13 +926,13 @@ def community_moderate(actor):
|
||||||
|
|
||||||
reports = Report.query.filter_by(status=0, in_community_id=community.id)
|
reports = Report.query.filter_by(status=0, in_community_id=community.id)
|
||||||
if local_remote == 'local':
|
if local_remote == 'local':
|
||||||
reports = reports.filter_by(ap_id=None)
|
reports = reports.filter(Report.source_instance_id == 1)
|
||||||
if local_remote == 'remote':
|
if local_remote == 'remote':
|
||||||
reports = reports.filter(Report.ap_id != None)
|
reports = reports.filter(Report.source_instance_id != 1)
|
||||||
reports = reports.order_by(desc(Report.created_at)).paginate(page=page, per_page=1000, error_out=False)
|
reports = reports.filter(Report.status >= 0).order_by(desc(Report.created_at)).paginate(page=page, per_page=1000, error_out=False)
|
||||||
|
|
||||||
next_url = url_for('admin.admin_reports', page=reports.next_num) if reports.has_next else None
|
next_url = url_for('community.community_moderate', page=reports.next_num) if reports.has_next else None
|
||||||
prev_url = url_for('admin.admin_reports', page=reports.prev_num) if reports.has_prev and page != 1 else None
|
prev_url = url_for('community.community_moderate', page=reports.prev_num) if reports.has_prev and page != 1 else None
|
||||||
|
|
||||||
return render_template('community/community_moderate.html', title=_('Moderation of %(community)s', community=community.display_name()),
|
return render_template('community/community_moderate.html', title=_('Moderation of %(community)s', community=community.display_name()),
|
||||||
community=community, reports=reports, current='reports',
|
community=community, reports=reports, current='reports',
|
||||||
|
@ -962,4 +965,61 @@ def community_moderate_banned(actor):
|
||||||
else:
|
else:
|
||||||
abort(401)
|
abort(401)
|
||||||
else:
|
else:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/community/<int:community_id>/moderate_report/<int:report_id>/escalate', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def community_moderate_report_escalate(community_id, report_id):
|
||||||
|
community = Community.query.get_or_404(community_id)
|
||||||
|
if community.is_moderator() or current_user.is_admin():
|
||||||
|
report = Report.query.filter_by(in_community_id=community.id, id=report_id, status=REPORT_STATE_NEW).first()
|
||||||
|
if report:
|
||||||
|
form = EscalateReportForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
notify = Notification(title='Escalated report', url='/admin/reports', user_id=1,
|
||||||
|
author_id=current_user.id)
|
||||||
|
db.session.add(notify)
|
||||||
|
report.description = form.reason.data
|
||||||
|
report.status = REPORT_STATE_ESCALATED
|
||||||
|
db.session.commit()
|
||||||
|
flash(_('Admin has been notified about this report.'))
|
||||||
|
# todo: remove unread notifications about this report
|
||||||
|
# todo: append to mod log
|
||||||
|
return redirect(url_for('community.community_moderate', actor=community.link()))
|
||||||
|
else:
|
||||||
|
form.reason.data = report.description
|
||||||
|
return render_template('community/community_moderate_report_escalate.html', form=form)
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/community/<int:community_id>/moderate_report/<int:report_id>/resolve', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def community_moderate_report_resolve(community_id, report_id):
|
||||||
|
community = Community.query.get_or_404(community_id)
|
||||||
|
if community.is_moderator() or current_user.is_admin():
|
||||||
|
report = Report.query.filter_by(in_community_id=community.id, id=report_id).first()
|
||||||
|
if report:
|
||||||
|
form = ResolveReportForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
report.status = REPORT_STATE_RESOLVED
|
||||||
|
db.session.commit()
|
||||||
|
# todo: remove unread notifications about this report
|
||||||
|
# todo: append to mod log
|
||||||
|
if form.also_resolve_others.data:
|
||||||
|
if report.suspect_post_reply_id:
|
||||||
|
db.session.execute(text('UPDATE "report" SET status = :new_status WHERE suspect_post_reply_id = :suspect_post_reply_id'),
|
||||||
|
{'new_status': REPORT_STATE_RESOLVED,
|
||||||
|
'suspect_post_reply_id': report.suspect_post_reply_id})
|
||||||
|
# todo: remove unread notifications about these reports
|
||||||
|
elif report.suspect_post_id:
|
||||||
|
db.session.execute(text('UPDATE "report" SET status = :new_status WHERE suspect_post_id = :suspect_post_id'),
|
||||||
|
{'new_status': REPORT_STATE_RESOLVED,
|
||||||
|
'suspect_post_id': report.suspect_post_id})
|
||||||
|
# todo: remove unread notifications about these reports
|
||||||
|
db.session.commit()
|
||||||
|
flash(_('Report resolved.'))
|
||||||
|
return redirect(url_for('community.community_moderate', actor=community.link()))
|
||||||
|
else:
|
||||||
|
return render_template('community/community_moderate_report_resolve.html', form=form)
|
||||||
|
|
|
@ -15,7 +15,7 @@ from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE
|
||||||
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
||||||
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, \
|
is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
|
||||||
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases
|
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases
|
||||||
from sqlalchemy import func, desc
|
from sqlalchemy import func, desc
|
||||||
import os
|
import os
|
||||||
|
@ -96,7 +96,7 @@ def retrieve_mods_and_backfill(community_id: int):
|
||||||
if outbox_request.status_code == 200:
|
if outbox_request.status_code == 200:
|
||||||
outbox_data = outbox_request.json()
|
outbox_data = outbox_request.json()
|
||||||
outbox_request.close()
|
outbox_request.close()
|
||||||
if outbox_data['type'] == 'OrderedCollection' and 'orderedItems' in outbox_data:
|
if 'type' in outbox_data and outbox_data['type'] == 'OrderedCollection' and 'orderedItems' in outbox_data:
|
||||||
activities_processed = 0
|
activities_processed = 0
|
||||||
for activity in outbox_data['orderedItems']:
|
for activity in outbox_data['orderedItems']:
|
||||||
user = find_actor_or_create(activity['object']['actor'])
|
user = find_actor_or_create(activity['object']['actor'])
|
||||||
|
@ -255,6 +255,7 @@ def save_post(form, post: Post):
|
||||||
|
|
||||||
# save the file
|
# save the file
|
||||||
final_place = os.path.join(directory, new_filename + file_ext)
|
final_place = os.path.join(directory, new_filename + file_ext)
|
||||||
|
final_place_medium = os.path.join(directory, new_filename + '_medium.webp')
|
||||||
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
|
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
|
||||||
uploaded_file.seek(0)
|
uploaded_file.seek(0)
|
||||||
uploaded_file.save(final_place)
|
uploaded_file.save(final_place)
|
||||||
|
@ -270,9 +271,11 @@ def save_post(form, post: Post):
|
||||||
img = ImageOps.exif_transpose(img)
|
img = ImageOps.exif_transpose(img)
|
||||||
img_width = img.width
|
img_width = img.width
|
||||||
img_height = img.height
|
img_height = img.height
|
||||||
|
img.thumbnail((2000, 2000))
|
||||||
|
img.save(final_place)
|
||||||
if img.width > 512 or img.height > 512:
|
if img.width > 512 or img.height > 512:
|
||||||
img.thumbnail((512, 512))
|
img.thumbnail((512, 512))
|
||||||
img.save(final_place)
|
img.save(final_place_medium, format="WebP", quality=93)
|
||||||
img_width = img.width
|
img_width = img.width
|
||||||
img_height = img.height
|
img_height = img.height
|
||||||
# save a second, smaller, version as a thumbnail
|
# save a second, smaller, version as a thumbnail
|
||||||
|
@ -281,7 +284,7 @@ def save_post(form, post: Post):
|
||||||
thumbnail_width = img.width
|
thumbnail_width = img.width
|
||||||
thumbnail_height = img.height
|
thumbnail_height = img.height
|
||||||
|
|
||||||
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=alt_text,
|
file = File(file_path=final_place_medium, file_name=new_filename + file_ext, alt_text=alt_text,
|
||||||
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
|
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
|
||||||
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail,
|
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail,
|
||||||
source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/"))
|
source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/"))
|
||||||
|
|
|
@ -16,4 +16,10 @@ SUBSCRIPTION_NONMEMBER = 0
|
||||||
SUBSCRIPTION_PENDING = -1
|
SUBSCRIPTION_PENDING = -1
|
||||||
SUBSCRIPTION_BANNED = -2
|
SUBSCRIPTION_BANNED = -2
|
||||||
|
|
||||||
THREAD_CUTOFF_DEPTH = 4
|
THREAD_CUTOFF_DEPTH = 4
|
||||||
|
|
||||||
|
REPORT_STATE_NEW = 0
|
||||||
|
REPORT_STATE_ESCALATED = 1
|
||||||
|
REPORT_STATE_APPEALED = 2
|
||||||
|
REPORT_STATE_RESOLVED = 3
|
||||||
|
REPORT_STATE_DISCARDED = -1
|
||||||
|
|
|
@ -440,6 +440,10 @@ class Community(db.Model):
|
||||||
retval = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
|
retval = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
|
||||||
return retval.lower()
|
return retval.lower()
|
||||||
|
|
||||||
|
def public_url(self):
|
||||||
|
result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
|
||||||
|
return result
|
||||||
|
|
||||||
def is_local(self):
|
def is_local(self):
|
||||||
return self.ap_id is None or self.profile_id().startswith('https://' + current_app.config['SERVER_NAME'])
|
return self.ap_id is None or self.profile_id().startswith('https://' + current_app.config['SERVER_NAME'])
|
||||||
|
|
||||||
|
@ -466,6 +470,14 @@ class Community(db.Model):
|
||||||
instances = instances.filter(Instance.id != 1, Instance.gone_forever == False)
|
instances = instances.filter(Instance.id != 1, Instance.gone_forever == False)
|
||||||
return instances.all()
|
return instances.all()
|
||||||
|
|
||||||
|
def has_followers_from_domain(self, domain: str) -> bool:
|
||||||
|
instances = Instance.query.join(User, User.instance_id == Instance.id).join(CommunityMember, CommunityMember.user_id == User.id)
|
||||||
|
instances = instances.filter(CommunityMember.community_id == self.id, CommunityMember.is_banned == False)
|
||||||
|
for instance in instances:
|
||||||
|
if instance.domain == domain:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_dependencies(self):
|
def delete_dependencies(self):
|
||||||
for post in self.posts:
|
for post in self.posts:
|
||||||
post.delete_dependencies()
|
post.delete_dependencies()
|
||||||
|
@ -750,6 +762,10 @@ class User(UserMixin, db.Model):
|
||||||
result = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
|
result = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def public_url(self):
|
||||||
|
result = self.ap_public_url if self.ap_public_url else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
|
||||||
|
return result
|
||||||
|
|
||||||
def created_recently(self):
|
def created_recently(self):
|
||||||
return self.created and self.created > utcnow() - timedelta(days=7)
|
return self.created and self.created > utcnow() - timedelta(days=7)
|
||||||
|
|
||||||
|
@ -803,6 +819,12 @@ class User(UserMixin, db.Model):
|
||||||
reply.body = reply.body_html = ''
|
reply.body = reply.body_html = ''
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
def mention_tag(self):
|
||||||
|
if self.ap_domain is None:
|
||||||
|
return '@' + self.user_name + '@' + current_app.config['SERVER_NAME']
|
||||||
|
else:
|
||||||
|
return '@' + self.user_name + '@' + self.ap_domain
|
||||||
|
|
||||||
|
|
||||||
class ActivityLog(db.Model):
|
class ActivityLog(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
@ -1171,7 +1193,7 @@ class Report(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
reasons = db.Column(db.String(256))
|
reasons = db.Column(db.String(256))
|
||||||
description = db.Column(db.String(256))
|
description = db.Column(db.String(256))
|
||||||
status = db.Column(db.Integer, default=0)
|
status = db.Column(db.Integer, default=0) # 0 = new, 1 = escalated to admin, 2 = being appealed, 3 = resolved, 4 = discarded
|
||||||
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
|
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
|
||||||
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
suspect_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
|
suspect_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
|
||||||
|
@ -1180,6 +1202,7 @@ class Report(db.Model):
|
||||||
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
|
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
|
||||||
suspect_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'))
|
suspect_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'))
|
||||||
in_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
|
in_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
|
||||||
|
source_instance_id = db.Column(db.Integer, db.ForeignKey('instance.id')) # the instance of the reporter. mostly used to distinguish between local (instance 1) and remote reports
|
||||||
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)
|
||||||
|
|
||||||
|
@ -1192,7 +1215,7 @@ class Report(db.Model):
|
||||||
return types[self.type]
|
return types[self.type]
|
||||||
|
|
||||||
def is_local(self):
|
def is_local(self):
|
||||||
return True
|
return self.source_instance_id == 1
|
||||||
|
|
||||||
|
|
||||||
class IpBan(db.Model):
|
class IpBan(db.Model):
|
||||||
|
|
|
@ -118,12 +118,12 @@ def show_post(post_id: int):
|
||||||
reply_json = {
|
reply_json = {
|
||||||
'type': 'Note',
|
'type': 'Note',
|
||||||
'id': reply.profile_id(),
|
'id': reply.profile_id(),
|
||||||
'attributedTo': current_user.profile_id(),
|
'attributedTo': current_user.public_url(),
|
||||||
'to': [
|
'to': [
|
||||||
'https://www.w3.org/ns/activitystreams#Public'
|
'https://www.w3.org/ns/activitystreams#Public'
|
||||||
],
|
],
|
||||||
'cc': [
|
'cc': [
|
||||||
community.profile_id(),
|
community.public_url(), post.author.public_url()
|
||||||
],
|
],
|
||||||
'content': reply.body_html,
|
'content': reply.body_html,
|
||||||
'inReplyTo': post.profile_id(),
|
'inReplyTo': post.profile_id(),
|
||||||
|
@ -134,20 +134,30 @@ def show_post(post_id: int):
|
||||||
},
|
},
|
||||||
'published': ap_datetime(utcnow()),
|
'published': ap_datetime(utcnow()),
|
||||||
'distinguished': False,
|
'distinguished': False,
|
||||||
'audience': community.profile_id()
|
'audience': community.public_url(),
|
||||||
|
'tag': [{
|
||||||
|
'href': post.author.public_url(),
|
||||||
|
'name': post.author.mention_tag(),
|
||||||
|
'type': 'Mention'
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
create_json = {
|
create_json = {
|
||||||
'type': 'Create',
|
'type': 'Create',
|
||||||
'actor': current_user.profile_id(),
|
'actor': current_user.public_url(),
|
||||||
'audience': community.profile_id(),
|
'audience': community.public_url(),
|
||||||
'to': [
|
'to': [
|
||||||
'https://www.w3.org/ns/activitystreams#Public'
|
'https://www.w3.org/ns/activitystreams#Public'
|
||||||
],
|
],
|
||||||
'cc': [
|
'cc': [
|
||||||
community.ap_profile_id
|
community.public_url(), post.author.public_url()
|
||||||
],
|
],
|
||||||
'object': reply_json,
|
'object': reply_json,
|
||||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||||
|
'tag': [{
|
||||||
|
'href': post.author.public_url(),
|
||||||
|
'name': post.author.mention_tag(),
|
||||||
|
'type': 'Mention'
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
if not community.is_local(): # this is a remote community, send it to the instance that hosts it
|
if not community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||||
success = post_request(community.ap_inbox_url, create_json, current_user.private_key,
|
success = post_request(community.ap_inbox_url, create_json, current_user.private_key,
|
||||||
|
@ -161,7 +171,7 @@ def show_post(post_id: int):
|
||||||
"to": [
|
"to": [
|
||||||
"https://www.w3.org/ns/activitystreams#Public"
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
],
|
],
|
||||||
"actor": community.ap_profile_id,
|
"actor": community.public_url(),
|
||||||
"cc": [
|
"cc": [
|
||||||
community.ap_followers_url
|
community.ap_followers_url
|
||||||
],
|
],
|
||||||
|
@ -173,6 +183,17 @@ def show_post(post_id: int):
|
||||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||||
send_to_remote_instance(instance.id, community.id, announce)
|
send_to_remote_instance(instance.id, community.id, announce)
|
||||||
|
|
||||||
|
# send copy of Note to post author (who won't otherwise get it if no-one else on their instance is subscribed to the community)
|
||||||
|
if not post.author.is_local() and post.author.ap_domain != community.ap_domain:
|
||||||
|
if not community.is_local() or (community.is_local and not community.has_followers_from_domain(post.author.ap_domain)):
|
||||||
|
success = post_request(post.author.ap_inbox_url, create_json, current_user.private_key,
|
||||||
|
current_user.ap_profile_id + '#main-key')
|
||||||
|
if not success:
|
||||||
|
# sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers
|
||||||
|
personal_inbox = post.author.public_url() + '/inbox'
|
||||||
|
post_request(personal_inbox, create_json, current_user.private_key,
|
||||||
|
current_user.ap_profile_id + '#main-key')
|
||||||
|
|
||||||
return redirect(url_for('activitypub.post_ap', post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
|
return redirect(url_for('activitypub.post_ap', post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
|
||||||
else:
|
else:
|
||||||
replies = post_replies(post.id, sort)
|
replies = post_replies(post.id, sort)
|
||||||
|
@ -518,14 +539,13 @@ def add_reply(post_id: int, comment_id: int):
|
||||||
reply_json = {
|
reply_json = {
|
||||||
'type': 'Note',
|
'type': 'Note',
|
||||||
'id': reply.profile_id(),
|
'id': reply.profile_id(),
|
||||||
'attributedTo': current_user.profile_id(),
|
'attributedTo': current_user.public_url(),
|
||||||
'to': [
|
'to': [
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public'
|
||||||
in_reply_to.author.profile_id()
|
|
||||||
],
|
],
|
||||||
'cc': [
|
'cc': [
|
||||||
post.community.profile_id(),
|
post.community.public_url(),
|
||||||
current_user.followers_url()
|
in_reply_to.author.public_url()
|
||||||
],
|
],
|
||||||
'content': reply.body_html,
|
'content': reply.body_html,
|
||||||
'inReplyTo': in_reply_to.profile_id(),
|
'inReplyTo': in_reply_to.profile_id(),
|
||||||
|
@ -537,7 +557,7 @@ def add_reply(post_id: int, comment_id: int):
|
||||||
},
|
},
|
||||||
'published': ap_datetime(utcnow()),
|
'published': ap_datetime(utcnow()),
|
||||||
'distinguished': False,
|
'distinguished': False,
|
||||||
'audience': post.community.profile_id(),
|
'audience': post.community.public_url(),
|
||||||
'contentMap': {
|
'contentMap': {
|
||||||
'en': reply.body_html
|
'en': reply.body_html
|
||||||
}
|
}
|
||||||
|
@ -545,15 +565,14 @@ def add_reply(post_id: int, comment_id: int):
|
||||||
create_json = {
|
create_json = {
|
||||||
'@context': default_context(),
|
'@context': default_context(),
|
||||||
'type': 'Create',
|
'type': 'Create',
|
||||||
'actor': current_user.profile_id(),
|
'actor': current_user.public_url(),
|
||||||
'audience': post.community.profile_id(),
|
'audience': post.community.public_url(),
|
||||||
'to': [
|
'to': [
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public'
|
||||||
in_reply_to.author.profile_id()
|
|
||||||
],
|
],
|
||||||
'cc': [
|
'cc': [
|
||||||
post.community.profile_id(),
|
post.community.public_url(),
|
||||||
current_user.followers_url()
|
in_reply_to.author.public_url()
|
||||||
],
|
],
|
||||||
'object': reply_json,
|
'object': reply_json,
|
||||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
|
||||||
|
@ -561,8 +580,15 @@ def add_reply(post_id: int, comment_id: int):
|
||||||
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
||||||
reply_json['tag'] = [
|
reply_json['tag'] = [
|
||||||
{
|
{
|
||||||
'href': in_reply_to.author.ap_profile_id,
|
'href': in_reply_to.author.public_url(),
|
||||||
'name': '@' + in_reply_to.author.ap_id,
|
'name': in_reply_to.author.mention_tag(),
|
||||||
|
'type': 'Mention'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
create_json['tag'] = [
|
||||||
|
{
|
||||||
|
'href': in_reply_to.author.public_url(),
|
||||||
|
'name': in_reply_to.author.mention_tag(),
|
||||||
'type': 'Mention'
|
'type': 'Mention'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -578,7 +604,7 @@ def add_reply(post_id: int, comment_id: int):
|
||||||
"to": [
|
"to": [
|
||||||
"https://www.w3.org/ns/activitystreams#Public"
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
],
|
],
|
||||||
"actor": post.community.ap_profile_id,
|
"actor": post.community.public_url(),
|
||||||
"cc": [
|
"cc": [
|
||||||
post.community.ap_followers_url
|
post.community.ap_followers_url
|
||||||
],
|
],
|
||||||
|
@ -590,6 +616,17 @@ def add_reply(post_id: int, comment_id: int):
|
||||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||||
send_to_remote_instance(instance.id, post.community.id, announce)
|
send_to_remote_instance(instance.id, post.community.id, announce)
|
||||||
|
|
||||||
|
# send copy of Note to comment author (who won't otherwise get it if no-one else on their instance is subscribed to the community)
|
||||||
|
if not in_reply_to.author.is_local() and in_reply_to.author.ap_domain != reply.community.ap_domain:
|
||||||
|
if not post.community.is_local() or (post.community.is_local and not post.community.has_followers_from_domain(in_reply_to.author.ap_domain)):
|
||||||
|
success = post_request(in_reply_to.author.ap_inbox_url, create_json, current_user.private_key,
|
||||||
|
current_user.ap_profile_id + '#main-key')
|
||||||
|
if not success:
|
||||||
|
# sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers
|
||||||
|
personal_inbox = in_reply_to.author.public_url() + '/inbox'
|
||||||
|
post_request(personal_inbox, create_json, current_user.private_key,
|
||||||
|
current_user.ap_profile_id + '#main-key')
|
||||||
|
|
||||||
if reply.depth <= constants.THREAD_CUTOFF_DEPTH:
|
if reply.depth <= constants.THREAD_CUTOFF_DEPTH:
|
||||||
return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{reply.id}'))
|
return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{reply.id}'))
|
||||||
else:
|
else:
|
||||||
|
@ -827,7 +864,7 @@ def post_report(post_id: int):
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||||
type=1, reporter_id=current_user.id, suspect_user_id=post.author.id, suspect_post_id=post.id,
|
type=1, reporter_id=current_user.id, suspect_user_id=post.author.id, suspect_post_id=post.id,
|
||||||
suspect_community_id=post.community.id, in_community_id=post.community.id)
|
suspect_community_id=post.community.id, in_community_id=post.community.id, source_instance_id=1)
|
||||||
db.session.add(report)
|
db.session.add(report)
|
||||||
|
|
||||||
# Notify moderators
|
# Notify moderators
|
||||||
|
@ -931,7 +968,8 @@ def post_reply_report(post_id: int, comment_id: int):
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||||
type=2, reporter_id=current_user.id, suspect_post_id=post.id, suspect_community_id=post.community.id,
|
type=2, reporter_id=current_user.id, suspect_post_id=post.id, suspect_community_id=post.community.id,
|
||||||
suspect_user_id=post_reply.author.id, suspect_post_reply_id=post_reply.id)
|
suspect_user_id=post_reply.author.id, suspect_post_reply_id=post_reply.id, in_community_id=post.community.id,
|
||||||
|
source_instance_id=1)
|
||||||
db.session.add(report)
|
db.session.add(report)
|
||||||
|
|
||||||
# Notify moderators
|
# Notify moderators
|
||||||
|
@ -1025,14 +1063,13 @@ def post_reply_edit(post_id: int, comment_id: int):
|
||||||
reply_json = {
|
reply_json = {
|
||||||
'type': 'Note',
|
'type': 'Note',
|
||||||
'id': post_reply.profile_id(),
|
'id': post_reply.profile_id(),
|
||||||
'attributedTo': current_user.profile_id(),
|
'attributedTo': current_user.public_url(),
|
||||||
'to': [
|
'to': [
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public'
|
||||||
in_reply_to.author.profile_id()
|
|
||||||
],
|
],
|
||||||
'cc': [
|
'cc': [
|
||||||
post.community.profile_id(),
|
post.community.public_url(),
|
||||||
current_user.followers_url()
|
in_reply_to.author.public_url()
|
||||||
],
|
],
|
||||||
'content': post_reply.body_html,
|
'content': post_reply.body_html,
|
||||||
'inReplyTo': in_reply_to.profile_id(),
|
'inReplyTo': in_reply_to.profile_id(),
|
||||||
|
@ -1045,37 +1082,54 @@ def post_reply_edit(post_id: int, comment_id: int):
|
||||||
'published': ap_datetime(post_reply.posted_at),
|
'published': ap_datetime(post_reply.posted_at),
|
||||||
'updated': ap_datetime(post_reply.edited_at),
|
'updated': ap_datetime(post_reply.edited_at),
|
||||||
'distinguished': False,
|
'distinguished': False,
|
||||||
'audience': post.community.profile_id(),
|
'audience': post.community.public_url(),
|
||||||
'contentMap': {
|
'contentMap': {
|
||||||
'en': post_reply.body_html
|
'en': post_reply.body_html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update_json = {
|
update_json = {
|
||||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
|
'@context': default_context(),
|
||||||
'type': 'Update',
|
'type': 'Update',
|
||||||
'actor': current_user.profile_id(),
|
'actor': current_user.public_url(),
|
||||||
'audience': post.community.profile_id(),
|
'audience': post.community.public_url(),
|
||||||
'to': [post.community.profile_id(), 'https://www.w3.org/ns/activitystreams#Public'],
|
'to': [
|
||||||
'published': ap_datetime(utcnow()),
|
'https://www.w3.org/ns/activitystreams#Public'
|
||||||
|
],
|
||||||
'cc': [
|
'cc': [
|
||||||
current_user.followers_url()
|
post.community.public_url(),
|
||||||
|
in_reply_to.author.public_url()
|
||||||
],
|
],
|
||||||
'object': reply_json,
|
'object': reply_json,
|
||||||
|
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}"
|
||||||
}
|
}
|
||||||
|
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
||||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
reply_json['tag'] = [
|
||||||
|
{
|
||||||
|
'href': in_reply_to.author.public_url(),
|
||||||
|
'name': in_reply_to.author.mention_tag(),
|
||||||
|
'type': 'Mention'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
update_json['tag'] = [
|
||||||
|
{
|
||||||
|
'href': in_reply_to.author.public_url(),
|
||||||
|
'name': in_reply_to.author.mention_tag(),
|
||||||
|
'type': 'Mention'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||||
success = post_request(post.community.ap_inbox_url, update_json, current_user.private_key,
|
success = post_request(post.community.ap_inbox_url, update_json, current_user.private_key,
|
||||||
current_user.ap_profile_id + '#main-key')
|
current_user.ap_profile_id + '#main-key')
|
||||||
if not success:
|
if not success:
|
||||||
flash('Failed to send edit to remote server', 'error')
|
flash('Failed to send send edit to remote server', 'error')
|
||||||
else: # local community - send it to followers on remote instances
|
else: # local community - send it to followers on remote instances
|
||||||
announce = {
|
announce = {
|
||||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||||
"type": 'Announce',
|
"type": 'Announce',
|
||||||
"to": [
|
"to": [
|
||||||
"https://www.w3.org/ns/activitystreams#Public"
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
],
|
],
|
||||||
"actor": post.community.ap_profile_id,
|
"actor": post.community.public_url(),
|
||||||
"cc": [
|
"cc": [
|
||||||
post.community.ap_followers_url
|
post.community.ap_followers_url
|
||||||
],
|
],
|
||||||
|
@ -1084,9 +1138,20 @@ def post_reply_edit(post_id: int, comment_id: int):
|
||||||
}
|
}
|
||||||
|
|
||||||
for instance in post.community.following_instances():
|
for instance in post.community.following_instances():
|
||||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(
|
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||||
instance.domain):
|
|
||||||
send_to_remote_instance(instance.id, post.community.id, announce)
|
send_to_remote_instance(instance.id, post.community.id, announce)
|
||||||
|
|
||||||
|
# send copy of Note to post author (who won't otherwise get it if no-one else on their instance is subscribed to the community)
|
||||||
|
if not in_reply_to.author.is_local() and in_reply_to.author.ap_domain != post_reply.community.ap_domain:
|
||||||
|
if not post.community.is_local() or (post.community.is_local and not post.community.has_followers_from_domain(in_reply_to.author.ap_domain)):
|
||||||
|
success = post_request(in_reply_to.author.ap_inbox_url, update_json, current_user.private_key,
|
||||||
|
current_user.ap_profile_id + '#main-key')
|
||||||
|
if not success:
|
||||||
|
# sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers
|
||||||
|
personal_inbox = in_reply_to.author.public_url() + '/inbox'
|
||||||
|
post_request(personal_inbox, update_json, current_user.private_key,
|
||||||
|
current_user.ap_profile_id + '#main-key')
|
||||||
|
|
||||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||||
else:
|
else:
|
||||||
form.body.data = post_reply.body
|
form.body.data = post_reply.body
|
||||||
|
|
|
@ -582,7 +582,11 @@ var DownArea = (function () {
|
||||||
if (self.textarea.selectionStart != self.textarea.selectionEnd) {
|
if (self.textarea.selectionStart != self.textarea.selectionEnd) {
|
||||||
end = self.textarea.value.substr(self.textarea.selectionEnd);
|
end = self.textarea.value.substr(self.textarea.selectionEnd);
|
||||||
var range = self.textarea.value.slice(self.textarea.selectionStart, self.textarea.selectionEnd);
|
var range = self.textarea.value.slice(self.textarea.selectionStart, self.textarea.selectionEnd);
|
||||||
blockquote = "".concat(blockquote).concat(range.trim());
|
var lines = range.trim().split('\n');
|
||||||
|
var modifiedLines = lines.map(function (line) {
|
||||||
|
return "> " + line.trim();
|
||||||
|
});
|
||||||
|
blockquote = modifiedLines.join('\n') + '\n';
|
||||||
}
|
}
|
||||||
if (start.length && start[start.length - 1] != '\n') {
|
if (start.length && start[start.length - 1] != '\n') {
|
||||||
blockquote = "\n".concat(blockquote);
|
blockquote = "\n".concat(blockquote);
|
||||||
|
|
|
@ -246,7 +246,7 @@
|
||||||
{% if post_layout == 'masonry' or post_layout == 'masonry_wide' %}
|
{% if post_layout == 'masonry' or post_layout == 'masonry_wide' %}
|
||||||
<!-- -->
|
<!-- -->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js', changed=getmtime('js/markdown/downarea.js')) }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
|
||||||
<script src="{{ url_for('static', filename='themes/' + theme() + '/scripts.js') }}" />
|
<script src="{{ url_for('static', filename='themes/' + theme() + '/scripts.js') }}" />
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
<div class="btn-group mt-1 mb-2">
|
<div class="btn-group mt-3 mb-2">
|
||||||
|
{% if community.is_owner() or current_user.is_admin() %}
|
||||||
|
<a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn {{ 'btn-primary' if current == '' or current == 'edit_settings' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
|
{{ _('Settings') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('community.community_mod_list', community_id=community.id) }}" class="btn {{ 'btn-primary' if current == 'moderators' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
|
{{ _('Moderators') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a href="/community/{{ community.link() }}/moderate" aria-label="{{ _('Sort by hot') }}" class="btn {{ 'btn-primary' if current == '' or current == 'reports' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
<a href="/community/{{ community.link() }}/moderate" aria-label="{{ _('Sort by hot') }}" class="btn {{ 'btn-primary' if current == '' or current == 'reports' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
{{ _('Reports') }}
|
{{ _('Reports') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/community/{{ community.link() }}/moderate/banned" class="btn {{ 'btn-primary' if current == 'banned' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
<a href="/community/{{ community.link() }}/moderate/banned" class="btn {{ 'btn-primary' if current == 'banned' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
{{ _('Banned people') }}
|
{{ _('Banned people') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/community/{{ community.link() }}/moderate/appeals" class="btn {{ 'btn-primary' if current == 'appeals' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
<a href="/community/{{ community.link() }}/moderate/appeals" class="btn {{ 'btn-primary' if current == 'appeals' else 'btn-outline-secondary' }} disabled" rel="nofollow noindex" >
|
||||||
{{ _('Appeals') }}
|
{{ _('Appeals') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="/community/{{ community.link() }}/moderate/modlog" class="btn {{ 'btn-primary' if current == 'modlog' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
<a href="/community/{{ community.link() }}/moderate/modlog" class="btn {{ 'btn-primary' if current == 'modlog' else 'btn-outline-secondary' }} disabled" rel="nofollow noindex" >
|
||||||
{{ _('Mod log') }}
|
{{ _('Mod log') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
|
@ -175,14 +175,10 @@
|
||||||
<h2>{{ _('Community Settings') }}</h2>
|
<h2>{{ _('Community Settings') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if is_moderator or is_owner or is_admin %}
|
|
||||||
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if is_owner or is_admin %}
|
{% if is_owner or is_admin %}
|
||||||
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings & Moderation') }}</a></p>
|
||||||
{% endif %}
|
{% elif is_moderator %}
|
||||||
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
|
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderation') }}</a></p>
|
||||||
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,9 @@
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
<div class="card-title">{{ _('Delete "%(community_title)s"', community_title=community.title) }}</div>
|
<div class="card-title">{{ _('Delete "%(community_title)s"', community_title=community.title) }}</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<p class="card-text"> Are you sure you want to delete this community? This is irreversible and will delete all posts and comments associated with it.</p>
|
||||||
{{ render_form(form) }}
|
{{ render_form(form) }}
|
||||||
|
<a class="btn btn-primary mt-2" href="/c/{{ community.link() }}">Go back</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,13 +15,17 @@
|
||||||
<li class="breadcrumb-item active">{{ _('Settings') }}</li>
|
<li class="breadcrumb-item active">{{ _('Settings') }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% if community %}
|
||||||
|
{% include "community/_community_moderation_nav.html" %}
|
||||||
|
{% endif %}
|
||||||
<h1 class="mt-2">
|
<h1 class="mt-2">
|
||||||
{% if community %}
|
{% if community %}
|
||||||
{{ _('Edit community') }}
|
{{ _('Edit %(community)s', community=community.display_name()) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _('Create community') }}
|
{{ _('Create community') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
|
<p>{{ _('Edit and configure this community') }}</p>
|
||||||
<form method="post" enctype="multipart/form-data" id="add_local_community_form" role="form">
|
<form method="post" enctype="multipart/form-data" id="add_local_community_form" role="form">
|
||||||
{{ form.csrf_token() }}
|
{{ form.csrf_token() }}
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
|
@ -48,10 +52,12 @@
|
||||||
{{ render_field(form.submit) }}
|
{{ render_field(form.submit) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<a class="btn btn-outline-secondary" href="{{ url_for('community.community_mod_list', community_id=community.id) }}">{{ _('Moderators') }}</a>
|
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
|
||||||
</div>
|
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
<li class="breadcrumb-item active">{{ _('Moderators') }}</li>
|
<li class="breadcrumb-item active">{{ _('Moderators') }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% include "community/_community_moderation_nav.html" %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10">
|
<div class="col-12 col-md-10">
|
||||||
<h1 class="mt-2">{{ _('Moderators for %(community)s', community=community.display_name()) }}</h1>
|
<h1 class="mt-2">{{ _('Moderators for %(community)s', community=community.display_name()) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<p>{{ _('See and change who moderates this community') }}</p>
|
||||||
<div class="col-12 col-md-2 text-right">
|
<div class="col-12 col-md-2 text-right">
|
||||||
<a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a>
|
<a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,16 +15,16 @@
|
||||||
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% include "community/_community_moderation_nav.html" %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10">
|
<div class="col-12 col-md-10">
|
||||||
<h1 class="mt-2">{{ _('Moderation of %(community)s', community=community.display_name()) }}</h1>
|
<h1 class="mt-2">{{ _('Reports') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-2 text-right">
|
<div class="col-12 col-md-2 text-right">
|
||||||
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
|
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include "community/_community_moderation_nav.html" %}
|
<p>{{ _('See and handle all reports made about %(community)s', community=community.display_name()) }}</p>
|
||||||
<h2>{{ _('Reports') }}</h2>
|
|
||||||
{% if reports.items %}
|
{% if reports.items %}
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<input type="search" name="search" value="{{ search }}">
|
<input type="search" name="search" value="{{ search }}">
|
||||||
|
@ -57,9 +57,9 @@
|
||||||
<a href="/post/{{ report.suspect_post_id }}">View</a>
|
<a href="/post/{{ report.suspect_post_id }}">View</a>
|
||||||
{% elif report.suspect_user_id %}
|
{% elif report.suspect_user_id %}
|
||||||
<a href="/user/{{ report.suspect_user_id }}">View</a>
|
<a href="/user/{{ report.suspect_user_id }}">View</a>
|
||||||
{% elif report.suspect_community_id %}
|
{% endif %} |
|
||||||
<a href="/user/{{ report.suspect_community_id }}">View</a>
|
<a href="{{ url_for('community.community_moderate_report_escalate', community_id=community.id, report_id=report.id) }}">{{ _('Escalate') }}</a> |
|
||||||
{% endif %}
|
<a href="{{ url_for('community.community_moderate_report_resolve', community_id=community.id, report_id=report.id) }}">{{ _('Resolve') }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -15,16 +15,17 @@
|
||||||
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% include "community/_community_moderation_nav.html" %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10">
|
<div class="col-12 col-md-10">
|
||||||
<h1 class="mt-2">{{ _('Moderation of %(community)s', community=community.display_name()) }}</h1>
|
<h1 class="mt-2">{{ _('Banned people') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-2 text-right">
|
<div class="col-12 col-md-2 text-right">
|
||||||
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
|
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include "community/_community_moderation_nav.html" %}
|
<p>{{ _('See and manage who is banned from %(community)s', community=community.display_name()) }}</p>
|
||||||
<h2>{{ _('Banned people') }}</h2>
|
<h2></h2>
|
||||||
{% if banned_people %}
|
{% if banned_people %}
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<input type="search" name="search" value="{{ search }}">
|
<input type="search" name="search" value="{{ search }}">
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('Escalate report to admins') }}</div>
|
||||||
|
<p>{{ _('For reports that could potentially involve legal issues or where you are unsure how to respond, you may prefer to let admins handle it.') }}</p>
|
||||||
|
{{ render_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('Resolve report') }}</div>
|
||||||
|
{{ render_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -82,8 +82,15 @@
|
||||||
<h2>{{ _('Community Settings') }}</h2>
|
<h2>{{ _('Community Settings') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
{% if is_moderator or is_owner or is_admin %}
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
<p><a href="/community/{{ post.community.link() }}/moderate" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_owner or is_admin %}
|
||||||
|
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if post.community.is_local() and (post.community.is_owner() or current_user.is_admin()) %}
|
||||||
|
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=post.community.id) }}" rel="nofollow">Delete community</a></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -133,8 +133,15 @@
|
||||||
<h2>{{ _('Community Settings') }}</h2>
|
<h2>{{ _('Community Settings') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
{% if is_moderator or is_owner or is_admin %}
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_owner or is_admin %}
|
||||||
|
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
|
||||||
|
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -227,8 +227,15 @@
|
||||||
<h2>{{ _('Community Settings') }}</h2>
|
<h2>{{ _('Community Settings') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
{% if is_moderator or is_owner or is_admin %}
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_owner or is_admin %}
|
||||||
|
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
|
||||||
|
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -78,8 +78,15 @@
|
||||||
<h2>{{ _('Community Settings') }}</h2>
|
<h2>{{ _('Community Settings') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
{% if is_moderator or is_owner or is_admin %}
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_owner or is_admin %}
|
||||||
|
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
|
||||||
|
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -159,7 +159,7 @@
|
||||||
{% if current_user.is_authenticated and (user_access('ban users', current_user.id) or user_access('manage users', current_user.id)) and user.id != current_user.id %}
|
{% if current_user.is_authenticated and (user_access('ban users', current_user.id) or user_access('manage users', current_user.id)) and user.id != current_user.id %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>{{ _('Crush') }}</h2>
|
<h2>{{ _('Moderate user') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -34,6 +34,12 @@ def show_people():
|
||||||
joined_communities=joined_communities(current_user.get_id()), title=_('People'))
|
joined_communities=joined_communities(current_user.get_id()), title=_('People'))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/user/<int:user_id>', methods=['GET'])
|
||||||
|
def show_profile_by_id(user_id):
|
||||||
|
user = User.query.get_or_404(user_id)
|
||||||
|
return show_profile(user)
|
||||||
|
|
||||||
|
|
||||||
def show_profile(user):
|
def show_profile(user):
|
||||||
if (user.deleted or user.banned) and current_user.is_anonymous:
|
if (user.deleted or user.banned) and current_user.is_anonymous:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
@ -332,7 +338,7 @@ def report_profile(actor):
|
||||||
if user and not user.banned:
|
if user and not user.banned:
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||||
type=0, reporter_id=current_user.id, suspect_user_id=user.id)
|
type=0, reporter_id=current_user.id, suspect_user_id=user.id, source_instance_id=1)
|
||||||
db.session.add(report)
|
db.session.add(report)
|
||||||
|
|
||||||
# Notify site admin
|
# Notify site admin
|
||||||
|
|
62
app/utils.py
62
app/utils.py
|
@ -214,41 +214,6 @@ def allowlist_html(html: str) -> str:
|
||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
# convert basic HTML to Markdown
|
|
||||||
def html_to_markdown(html: str) -> str:
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
return html_to_markdown_worker(soup)
|
|
||||||
|
|
||||||
|
|
||||||
def html_to_markdown_worker(element, indent_level=0):
|
|
||||||
formatted_text = ''
|
|
||||||
for item in element.contents:
|
|
||||||
if isinstance(item, str):
|
|
||||||
formatted_text += item
|
|
||||||
elif item.name == 'p':
|
|
||||||
formatted_text += '\n\n'
|
|
||||||
elif item.name == 'br':
|
|
||||||
formatted_text += ' \n' # Double space at the end for line break
|
|
||||||
elif item.name == 'strong':
|
|
||||||
formatted_text += '**' + html_to_markdown_worker(item) + '**'
|
|
||||||
elif item.name == 'ul':
|
|
||||||
formatted_text += '\n'
|
|
||||||
formatted_text += html_to_markdown_worker(item, indent_level + 1)
|
|
||||||
formatted_text += '\n'
|
|
||||||
elif item.name == 'ol':
|
|
||||||
formatted_text += '\n'
|
|
||||||
formatted_text += html_to_markdown_worker(item, indent_level + 1)
|
|
||||||
formatted_text += '\n'
|
|
||||||
elif item.name == 'li':
|
|
||||||
bullet = '-' if item.find_parent(['ul', 'ol']) and item.find_previous_sibling() is None else ''
|
|
||||||
formatted_text += ' ' * indent_level + bullet + ' ' + html_to_markdown_worker(item).strip() + '\n'
|
|
||||||
elif item.name == 'blockquote':
|
|
||||||
formatted_text += ' ' * indent_level + '> ' + html_to_markdown_worker(item).strip() + '\n'
|
|
||||||
elif item.name == 'code':
|
|
||||||
formatted_text += '`' + html_to_markdown_worker(item) + '`'
|
|
||||||
return formatted_text
|
|
||||||
|
|
||||||
|
|
||||||
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, 'strike': True}))
|
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'strike': True}))
|
||||||
|
@ -262,6 +227,31 @@ def markdown_to_text(markdown_text) -> str:
|
||||||
return markdown_text.replace("# ", '')
|
return markdown_text.replace("# ", '')
|
||||||
|
|
||||||
|
|
||||||
|
def microblog_content_to_title(html: str) -> str:
|
||||||
|
soup = BeautifulSoup(html, 'html.parser')
|
||||||
|
|
||||||
|
title_found = False
|
||||||
|
for tag in soup.find_all():
|
||||||
|
if tag.name == 'p':
|
||||||
|
if not title_found:
|
||||||
|
title_found = True
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
tag = tag.extract()
|
||||||
|
|
||||||
|
if title_found:
|
||||||
|
result = soup.text
|
||||||
|
if len(result) > 150:
|
||||||
|
for i in range(149, -1, -1):
|
||||||
|
if result[i] == ' ':
|
||||||
|
break;
|
||||||
|
result = result[:i] + ' ...' if i > 0 else ''
|
||||||
|
else:
|
||||||
|
result = ''
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def domain_from_url(url: str, create=True) -> Domain:
|
def domain_from_url(url: str, create=True) -> Domain:
|
||||||
parsed_url = urlparse(url.lower().replace('www.', ''))
|
parsed_url = urlparse(url.lower().replace('www.', ''))
|
||||||
if parsed_url and parsed_url.hostname:
|
if parsed_url and parsed_url.hostname:
|
||||||
|
@ -670,7 +660,7 @@ def finalize_user_setup(user, application_required=False):
|
||||||
private_key, public_key = RsaKeys.generate_keypair()
|
private_key, public_key = RsaKeys.generate_keypair()
|
||||||
user.private_key = private_key
|
user.private_key = private_key
|
||||||
user.public_key = public_key
|
user.public_key = public_key
|
||||||
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}".lower()
|
||||||
user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
||||||
user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
|
user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
34
migrations/versions/04697ae91fac_report_source.py
Normal file
34
migrations/versions/04697ae91fac_report_source.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"""report source
|
||||||
|
|
||||||
|
Revision ID: 04697ae91fac
|
||||||
|
Revises: 2b028a70bd7a
|
||||||
|
Create Date: 2024-03-26 22:13:16.749010
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '04697ae91fac'
|
||||||
|
down_revision = '2b028a70bd7a'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('report', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('source_instance_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key('fk_report_source_instance_id', 'instance', ['source_instance_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('report', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('fk_report_source_instance_id', type_='foreignkey')
|
||||||
|
batch_op.drop_column('source_instance_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue