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, \
|
||||
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, \
|
||||
update_post_from_activity, undo_vote, undo_downvote
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \
|
||||
update_post_from_activity, undo_vote, undo_downvote, post_to_page
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \
|
||||
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, \
|
||||
community_moderators
|
||||
import werkzeug.exceptions
|
||||
|
@ -175,12 +175,12 @@ def user_profile(actor):
|
|||
actor = actor.strip()
|
||||
if current_user.is_authenticated and current_user.is_admin():
|
||||
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:
|
||||
user: User = User.query.filter_by(user_name=actor, ap_id=None).first()
|
||||
else:
|
||||
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:
|
||||
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'])),
|
||||
encrypted=encrypted)
|
||||
db.session.add(new_message)
|
||||
existing_conversation.updated_at = utcnow()
|
||||
db.session.commit()
|
||||
|
||||
# Notify recipient
|
||||
|
@ -432,20 +433,43 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
activity_log.result = 'success'
|
||||
else:
|
||||
try:
|
||||
community_ap_id = request_json['to'][0]
|
||||
if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public': # kbin does this when posting a reply
|
||||
if 'to' in request_json['object'] and request_json['object']['to']:
|
||||
community_ap_id = request_json['object']['to'][0]
|
||||
if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public' and 'cc' in \
|
||||
request_json['object'] and request_json['object']['cc']:
|
||||
community_ap_id = request_json['object']['cc'][0]
|
||||
elif 'cc' in request_json['object'] and request_json['object']['cc']:
|
||||
community_ap_id = request_json['object']['cc'][0]
|
||||
if community_ap_id.endswith('/followers'): # mastodon
|
||||
if '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
|
||||
community_ap_id = ''
|
||||
locations = ['audience', 'cc', 'to']
|
||||
if 'object' in request_json:
|
||||
rjs = [ request_json, request_json['object'] ]
|
||||
else:
|
||||
rjs = [ request_json ]
|
||||
local_community_prefix = f"https://{current_app.config['SERVER_NAME']}/c/"
|
||||
followers_suffix = '/followers'
|
||||
for rj in rjs:
|
||||
for loc in locations:
|
||||
if loc in rj:
|
||||
id = rj[loc]
|
||||
if isinstance(id, str):
|
||||
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:
|
||||
activity_log.activity_type = 'exception'
|
||||
db.session.commit()
|
||||
|
@ -1029,6 +1053,27 @@ def community_outbox(actor):
|
|||
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'])
|
||||
def community_moderators_route(actor):
|
||||
actor = actor.strip()
|
||||
|
@ -1069,7 +1114,7 @@ def community_followers(actor):
|
|||
if community is not None:
|
||||
result = {
|
||||
"@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",
|
||||
"totalItems": community_members(community.id),
|
||||
"items": []
|
||||
|
|
|
@ -22,10 +22,10 @@ from PIL import Image, ImageOps
|
|||
from io import BytesIO
|
||||
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, \
|
||||
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():
|
||||
|
@ -178,6 +178,43 @@ def post_to_activity(post: Post, community: Community):
|
|||
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():
|
||||
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
|
||||
if user is None:
|
||||
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 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)
|
||||
elif 'content' in activity_json:
|
||||
community.description_html = allowlist_html(activity_json['content'])
|
||||
community.description = html_to_markdown(community.description_html)
|
||||
community.description = ''
|
||||
|
||||
icon_changed = cover_changed = False
|
||||
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_inbox_url=activity_json['endpoints']['sharedInbox'],
|
||||
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_fetched_at=utcnow(),
|
||||
ap_domain=server,
|
||||
|
@ -580,7 +622,7 @@ def actor_json_to_model(activity_json, address, server):
|
|||
community.description_html = markdown_to_html(community.description)
|
||||
elif 'content' in activity_json:
|
||||
community.description_html = allowlist_html(activity_json['content'])
|
||||
community.description = html_to_markdown(community.description_html)
|
||||
community.description = ''
|
||||
if 'icon' in activity_json:
|
||||
icon = File(source_url=activity_json['icon']['url'])
|
||||
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)
|
||||
elif 'content' in post_json:
|
||||
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 post_json['attachment'][0]['type'] == 'Link':
|
||||
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':
|
||||
# Convert Markdown to HTML
|
||||
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
|
||||
elif 'summary' in user_json:
|
||||
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)
|
||||
elif 'content' in request_json['object']: # Kbin
|
||||
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:
|
||||
# Discard post_reply if it contains certain phrases. Good for stopping spam floods.
|
||||
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.result = 'ignored'
|
||||
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.
|
||||
return None
|
||||
nsfl_in_title = '[NSFL]' in request_json['object']['name'].upper() or '(NSFL)' in request_json['object']['name'].upper()
|
||||
if 'name' not in request_json['object']: # Microblog posts
|
||||
if 'content' in request_json['object'] and request_json['object']['content'] is not None:
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
|
@ -1278,7 +1326,12 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
|||
post.body_html = markdown_to_html(post.body)
|
||||
elif 'content' in request_json['object'] and request_json['object']['content'] is not None: # Kbin
|
||||
post.body_html = allowlist_html(request_json['object']['content'])
|
||||
post.body = html_to_markdown(post.body_html)
|
||||
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.
|
||||
blocked_phrases_list = blocked_phrases()
|
||||
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 \
|
||||
'type' in request_json['object']['attachment'][0]:
|
||||
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):
|
||||
post.type = POST_TYPE_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)
|
||||
elif 'content' in request_json['object']:
|
||||
reply.body_html = allowlist_html(request_json['object']['content'])
|
||||
reply.body = html_to_markdown(reply.body_html)
|
||||
reply.body = ''
|
||||
reply.edited_at = utcnow()
|
||||
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)
|
||||
elif 'content' in request_json['object']:
|
||||
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']:
|
||||
post.url = request_json['object']['attachment']['href']
|
||||
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_login import login_required, current_user
|
||||
from flask_babel import _
|
||||
from sqlalchemy import text, desc
|
||||
from sqlalchemy import text, desc, or_
|
||||
|
||||
from app import db, celery, cache
|
||||
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, \
|
||||
topic_tree, topics_for_form
|
||||
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, \
|
||||
User, Instance, File, Report, Topic, UserRegistration, Role, Post
|
||||
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()
|
||||
user.private_key = private_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_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
|
||||
user.roles.append(Role.query.get(form.role.data))
|
||||
|
@ -674,7 +675,7 @@ def admin_reports():
|
|||
search = request.args.get('search', '')
|
||||
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':
|
||||
reports = reports.filter_by(ap_id=None)
|
||||
if local_remote == 'remote':
|
||||
|
|
|
@ -99,7 +99,7 @@ def register():
|
|||
flash(_('Sorry, you cannot use that user name'), 'error')
|
||||
else:
|
||||
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.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
||||
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,
|
||||
verification_token=verification_token, instance_id=1, ip_address=ip_address(),
|
||||
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)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
|
|
@ -145,7 +145,7 @@ def chat_report(conversation_id):
|
|||
|
||||
if form.validate_on_submit():
|
||||
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)
|
||||
|
||||
# Notify site admin
|
||||
|
|
|
@ -13,6 +13,7 @@ def send_message(message: str, conversation_id: int) -> ChatMessage:
|
|||
conversation = Conversation.query.get(conversation_id)
|
||||
reply = ChatMessage(sender_id=current_user.id, conversation_id=conversation.id,
|
||||
body=message, body_html=allowlist_html(markdown_to_html(message)))
|
||||
conversation.updated_at = utcnow()
|
||||
for recipient in conversation.members:
|
||||
if recipient.id != current_user.id:
|
||||
if recipient.is_local():
|
||||
|
|
|
@ -3,7 +3,7 @@ from flask_login import current_user
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField, \
|
||||
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 app import db
|
||||
|
@ -61,6 +61,17 @@ class AddModeratorForm(FlaskForm):
|
|||
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):
|
||||
address = StringField(_l('Community address'), render_kw={'placeholder': 'e.g. !name@server', 'autofocus': True}, validators=[DataRequired()])
|
||||
submit = SubmitField(_l('Search'))
|
||||
|
@ -77,15 +88,15 @@ class BanUserCommunityForm(FlaskForm):
|
|||
class CreatePostForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
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_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_body = TextAreaField(_l('Body'), validators={Optional(), Length(min=3, max=5000)},
|
||||
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)'})
|
||||
link_title = StringField(_l('Title'), validators=[Optional(), Length(min=3, max=255)])
|
||||
link_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)],
|
||||
render_kw={'placeholder': 'Text (optional)'})
|
||||
link_url = StringField(_l('URL'), render_kw={'placeholder': 'https://...'})
|
||||
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_body = TextAreaField(_l('Body'), validators={Optional(), Length(min=3, max=5000)},
|
||||
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_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)],
|
||||
render_kw={'placeholder': 'Text (optional)'})
|
||||
image_file = FileField(_('Image'))
|
||||
# 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_login import current_user, login_required
|
||||
from flask_babel import _
|
||||
from sqlalchemy import or_, desc
|
||||
from sqlalchemy import or_, desc, text
|
||||
|
||||
from app import db, constants, cache
|
||||
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.chat.util import send_message
|
||||
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, \
|
||||
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
|
||||
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.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||
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,
|
||||
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,
|
||||
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_domain=current_app.config['SERVER_NAME'],
|
||||
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')
|
||||
else:
|
||||
flash(_('Community not found. If you are searching for a nsfw community it is blocked by this instance.'), 'warning')
|
||||
|
||||
if new_community.banned:
|
||||
flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning')
|
||||
else:
|
||||
if new_community.banned:
|
||||
flash(_('That community is banned from %(site)s.', site=g.site.name), 'warning')
|
||||
|
||||
return render_template('community/add_remote.html',
|
||||
title=_('Add remote community'), form=form, new_community=new_community,
|
||||
|
@ -577,7 +579,7 @@ def community_report(community_id: int):
|
|||
form = ReportCommunityForm()
|
||||
if form.validate_on_submit():
|
||||
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)
|
||||
|
||||
# Notify admin
|
||||
|
@ -638,7 +640,8 @@ def community_edit(community_id: int):
|
|||
community.image = file
|
||||
|
||||
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()
|
||||
flash(_('Saved'))
|
||||
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.default_layout.data = community.default_layout
|
||||
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()),
|
||||
joined_communities=joined_communities(current_user.get_id()))
|
||||
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()
|
||||
|
||||
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()),
|
||||
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)
|
||||
if local_remote == 'local':
|
||||
reports = reports.filter_by(ap_id=None)
|
||||
reports = reports.filter(Report.source_instance_id == 1)
|
||||
if local_remote == 'remote':
|
||||
reports = reports.filter(Report.ap_id != None)
|
||||
reports = reports.order_by(desc(Report.created_at)).paginate(page=page, per_page=1000, error_out=False)
|
||||
reports = reports.filter(Report.source_instance_id != 1)
|
||||
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
|
||||
prev_url = url_for('admin.admin_reports', page=reports.prev_num) if reports.has_prev and page != 1 else None
|
||||
next_url = url_for('community.community_moderate', page=reports.next_num) if reports.has_next 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()),
|
||||
community=community, reports=reports, current='reports',
|
||||
|
@ -962,4 +965,61 @@ def community_moderate_banned(actor):
|
|||
else:
|
||||
abort(401)
|
||||
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, \
|
||||
Instance, Notification, User, ActivityPubLog
|
||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
|
||||
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
|
||||
is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
|
||||
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases
|
||||
from sqlalchemy import func, desc
|
||||
import os
|
||||
|
@ -96,7 +96,7 @@ def retrieve_mods_and_backfill(community_id: int):
|
|||
if outbox_request.status_code == 200:
|
||||
outbox_data = outbox_request.json()
|
||||
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
|
||||
for activity in outbox_data['orderedItems']:
|
||||
user = find_actor_or_create(activity['object']['actor'])
|
||||
|
@ -255,6 +255,7 @@ def save_post(form, post: Post):
|
|||
|
||||
# save the file
|
||||
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')
|
||||
uploaded_file.seek(0)
|
||||
uploaded_file.save(final_place)
|
||||
|
@ -270,9 +271,11 @@ def save_post(form, post: Post):
|
|||
img = ImageOps.exif_transpose(img)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
img.thumbnail((2000, 2000))
|
||||
img.save(final_place)
|
||||
if img.width > 512 or img.height > 512:
|
||||
img.thumbnail((512, 512))
|
||||
img.save(final_place)
|
||||
img.save(final_place_medium, format="WebP", quality=93)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
# save a second, smaller, version as a thumbnail
|
||||
|
@ -281,7 +284,7 @@ def save_post(form, post: Post):
|
|||
thumbnail_width = img.width
|
||||
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,
|
||||
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail,
|
||||
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_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}"
|
||||
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):
|
||||
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)
|
||||
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):
|
||||
for post in self.posts:
|
||||
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}"
|
||||
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):
|
||||
return self.created and self.created > utcnow() - timedelta(days=7)
|
||||
|
||||
|
@ -803,6 +819,12 @@ class User(UserMixin, db.Model):
|
|||
reply.body = reply.body_html = ''
|
||||
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):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -1171,7 +1193,7 @@ class Report(db.Model):
|
|||
id = db.Column(db.Integer, primary_key=True)
|
||||
reasons = 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
|
||||
reporter_id = db.Column(db.Integer, db.ForeignKey('user.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_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.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)
|
||||
updated = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
@ -1192,7 +1215,7 @@ class Report(db.Model):
|
|||
return types[self.type]
|
||||
|
||||
def is_local(self):
|
||||
return True
|
||||
return self.source_instance_id == 1
|
||||
|
||||
|
||||
class IpBan(db.Model):
|
||||
|
|
|
@ -118,12 +118,12 @@ def show_post(post_id: int):
|
|||
reply_json = {
|
||||
'type': 'Note',
|
||||
'id': reply.profile_id(),
|
||||
'attributedTo': current_user.profile_id(),
|
||||
'attributedTo': current_user.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
community.profile_id(),
|
||||
community.public_url(), post.author.public_url()
|
||||
],
|
||||
'content': reply.body_html,
|
||||
'inReplyTo': post.profile_id(),
|
||||
|
@ -134,20 +134,30 @@ def show_post(post_id: int):
|
|||
},
|
||||
'published': ap_datetime(utcnow()),
|
||||
'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 = {
|
||||
'type': 'Create',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': community.profile_id(),
|
||||
'actor': current_user.public_url(),
|
||||
'audience': community.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
community.ap_profile_id
|
||||
community.public_url(), post.author.public_url()
|
||||
],
|
||||
'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
|
||||
success = post_request(community.ap_inbox_url, create_json, current_user.private_key,
|
||||
|
@ -161,7 +171,7 @@ def show_post(post_id: int):
|
|||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": community.ap_profile_id,
|
||||
"actor": community.public_url(),
|
||||
"cc": [
|
||||
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):
|
||||
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
|
||||
else:
|
||||
replies = post_replies(post.id, sort)
|
||||
|
@ -518,14 +539,13 @@ def add_reply(post_id: int, comment_id: int):
|
|||
reply_json = {
|
||||
'type': 'Note',
|
||||
'id': reply.profile_id(),
|
||||
'attributedTo': current_user.profile_id(),
|
||||
'attributedTo': current_user.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'content': reply.body_html,
|
||||
'inReplyTo': in_reply_to.profile_id(),
|
||||
|
@ -537,7 +557,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
},
|
||||
'published': ap_datetime(utcnow()),
|
||||
'distinguished': False,
|
||||
'audience': post.community.profile_id(),
|
||||
'audience': post.community.public_url(),
|
||||
'contentMap': {
|
||||
'en': reply.body_html
|
||||
}
|
||||
|
@ -545,15 +565,14 @@ def add_reply(post_id: int, comment_id: int):
|
|||
create_json = {
|
||||
'@context': default_context(),
|
||||
'type': 'Create',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'actor': current_user.public_url(),
|
||||
'audience': post.community.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'object': reply_json,
|
||||
'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:
|
||||
reply_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.ap_profile_id,
|
||||
'name': '@' + in_reply_to.author.ap_id,
|
||||
'href': in_reply_to.author.public_url(),
|
||||
'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'
|
||||
}
|
||||
]
|
||||
|
@ -578,7 +604,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": post.community.ap_profile_id,
|
||||
"actor": post.community.public_url(),
|
||||
"cc": [
|
||||
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):
|
||||
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:
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{reply.id}'))
|
||||
else:
|
||||
|
@ -827,7 +864,7 @@ def post_report(post_id: int):
|
|||
if form.validate_on_submit():
|
||||
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,
|
||||
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)
|
||||
|
||||
# Notify moderators
|
||||
|
@ -931,7 +968,8 @@ def post_reply_report(post_id: int, comment_id: int):
|
|||
if form.validate_on_submit():
|
||||
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,
|
||||
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)
|
||||
|
||||
# Notify moderators
|
||||
|
@ -1025,14 +1063,13 @@ def post_reply_edit(post_id: int, comment_id: int):
|
|||
reply_json = {
|
||||
'type': 'Note',
|
||||
'id': post_reply.profile_id(),
|
||||
'attributedTo': current_user.profile_id(),
|
||||
'attributedTo': current_user.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'content': post_reply.body_html,
|
||||
'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),
|
||||
'updated': ap_datetime(post_reply.edited_at),
|
||||
'distinguished': False,
|
||||
'audience': post.community.profile_id(),
|
||||
'audience': post.community.public_url(),
|
||||
'contentMap': {
|
||||
'en': post_reply.body_html
|
||||
}
|
||||
}
|
||||
update_json = {
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
|
||||
'@context': default_context(),
|
||||
'type': 'Update',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'to': [post.community.profile_id(), 'https://www.w3.org/ns/activitystreams#Public'],
|
||||
'published': ap_datetime(utcnow()),
|
||||
'actor': current_user.public_url(),
|
||||
'audience': post.community.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'object': reply_json,
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}"
|
||||
}
|
||||
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
||||
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,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
flash('Failed to send edit to remote server', 'error')
|
||||
else: # local community - send it to followers on remote instances
|
||||
flash('Failed to send send edit to remote server', 'error')
|
||||
else: # local community - send it to followers on remote instances
|
||||
announce = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||
"type": 'Announce',
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": post.community.ap_profile_id,
|
||||
"actor": post.community.public_url(),
|
||||
"cc": [
|
||||
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():
|
||||
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 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))
|
||||
else:
|
||||
form.body.data = post_reply.body
|
||||
|
|
|
@ -582,7 +582,11 @@ var DownArea = (function () {
|
|||
if (self.textarea.selectionStart != self.textarea.selectionEnd) {
|
||||
end = self.textarea.value.substr(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') {
|
||||
blockquote = "\n".concat(blockquote);
|
||||
|
|
|
@ -246,7 +246,7 @@
|
|||
{% if post_layout == 'masonry' or post_layout == 'masonry_wide' %}
|
||||
<!-- -->
|
||||
{% 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 %}
|
||||
{% if theme() and file_exists('app/templates/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">
|
||||
{{ _('Reports') }}
|
||||
</a>
|
||||
<a href="/community/{{ community.link() }}/moderate/banned" class="btn {{ 'btn-primary' if current == 'banned' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||
{{ _('Banned people') }}
|
||||
</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') }}
|
||||
</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') }}
|
||||
</a>
|
||||
</div>
|
|
@ -175,14 +175,10 @@
|
|||
<h2>{{ _('Community Settings') }}</h2>
|
||||
</div>
|
||||
<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 %}
|
||||
<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>
|
||||
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings & Moderation') }}</a></p>
|
||||
{% elif is_moderator %}
|
||||
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderation') }}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Delete "%(community_title)s"', community_title=community.title) }}</div>
|
||||
<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) }}
|
||||
<a class="btn btn-primary mt-2" href="/c/{{ community.link() }}">Go back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,13 +15,17 @@
|
|||
<li class="breadcrumb-item active">{{ _('Settings') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% if community %}
|
||||
{% include "community/_community_moderation_nav.html" %}
|
||||
{% endif %}
|
||||
<h1 class="mt-2">
|
||||
{% if community %}
|
||||
{{ _('Edit community') }}
|
||||
{{ _('Edit %(community)s', community=community.display_name()) }}
|
||||
{% else %}
|
||||
{{ _('Create community') }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
<p>{{ _('Edit and configure this community') }}</p>
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_community_form" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.title) }}
|
||||
|
@ -48,10 +52,12 @@
|
|||
{{ render_field(form.submit) }}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('community.community_mod_list', community_id=community.id) }}">{{ _('Moderators') }}</a>
|
||||
</div>
|
||||
{% 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>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
<li class="breadcrumb-item active">{{ _('Moderators') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% include "community/_community_moderation_nav.html" %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10">
|
||||
<h1 class="mt-2">{{ _('Moderators for %(community)s', community=community.display_name()) }}</h1>
|
||||
</div>
|
||||
<p>{{ _('See and change who moderates this community') }}</p>
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
@ -15,16 +15,16 @@
|
|||
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% include "community/_community_moderation_nav.html" %}
|
||||
<div class="row">
|
||||
<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 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> -->
|
||||
</div>
|
||||
</div>
|
||||
{% include "community/_community_moderation_nav.html" %}
|
||||
<h2>{{ _('Reports') }}</h2>
|
||||
<p>{{ _('See and handle all reports made about %(community)s', community=community.display_name()) }}</p>
|
||||
{% if reports.items %}
|
||||
<form method="get">
|
||||
<input type="search" name="search" value="{{ search }}">
|
||||
|
@ -57,9 +57,9 @@
|
|||
<a href="/post/{{ report.suspect_post_id }}">View</a>
|
||||
{% elif report.suspect_user_id %}
|
||||
<a href="/user/{{ report.suspect_user_id }}">View</a>
|
||||
{% elif report.suspect_community_id %}
|
||||
<a href="/user/{{ report.suspect_community_id }}">View</a>
|
||||
{% endif %}
|
||||
{% endif %} |
|
||||
<a href="{{ url_for('community.community_moderate_report_escalate', community_id=community.id, report_id=report.id) }}">{{ _('Escalate') }}</a> |
|
||||
<a href="{{ url_for('community.community_moderate_report_resolve', community_id=community.id, report_id=report.id) }}">{{ _('Resolve') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -15,16 +15,17 @@
|
|||
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% include "community/_community_moderation_nav.html" %}
|
||||
<div class="row">
|
||||
<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 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> -->
|
||||
</div>
|
||||
</div>
|
||||
{% include "community/_community_moderation_nav.html" %}
|
||||
<h2>{{ _('Banned people') }}</h2>
|
||||
<p>{{ _('See and manage who is banned from %(community)s', community=community.display_name()) }}</p>
|
||||
<h2></h2>
|
||||
{% if banned_people %}
|
||||
<form method="get">
|
||||
<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>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||
{% if is_moderator or is_owner or is_admin %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
|
|
@ -133,8 +133,15 @@
|
|||
<h2>{{ _('Community Settings') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||
{% 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 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
|
|
@ -227,8 +227,15 @@
|
|||
<h2>{{ _('Community Settings') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||
{% 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 %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
|
|
@ -78,8 +78,15 @@
|
|||
<h2>{{ _('Community Settings') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||
{% 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 %}
|
||||
<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>
|
||||
{% 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 %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Crush') }}</h2>
|
||||
<h2>{{ _('Moderate user') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
|
|
|
@ -34,6 +34,12 @@ def show_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):
|
||||
if (user.deleted or user.banned) and current_user.is_anonymous:
|
||||
abort(404)
|
||||
|
@ -332,7 +338,7 @@ def report_profile(actor):
|
|||
if user and not user.banned:
|
||||
if form.validate_on_submit():
|
||||
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)
|
||||
|
||||
# Notify site admin
|
||||
|
|
62
app/utils.py
62
app/utils.py
|
@ -214,41 +214,6 @@ def allowlist_html(html: str) -> str:
|
|||
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:
|
||||
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}))
|
||||
|
@ -262,6 +227,31 @@ def markdown_to_text(markdown_text) -> str:
|
|||
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:
|
||||
parsed_url = urlparse(url.lower().replace('www.', ''))
|
||||
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()
|
||||
user.private_key = private_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_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
|
||||
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