comment edit, delete, report, block

This commit is contained in:
rimu 2023-12-28 20:00:07 +13:00
parent 8def5b9afc
commit 0b159edf0a
15 changed files with 337 additions and 12 deletions

View file

@ -425,7 +425,8 @@ def process_inbox_request(request_json, activitypublog_id):
score=instance_weight(user.ap_domain), score=instance_weight(user.ap_domain),
ap_id=request_json['object']['id'], ap_id=request_json['object']['id'],
ap_create_id=request_json['id'], ap_create_id=request_json['id'],
ap_announce_id=None) ap_announce_id=None,
instance_id=user.instance_id)
if 'source' in request_json['object'] and \ if 'source' in request_json['object'] and \
request_json['object']['source']['mediaType'] == 'text/markdown': request_json['object']['source']['mediaType'] == 'text/markdown':
post_reply.body = request_json['object']['source']['content'] post_reply.body = request_json['object']['source']['content']
@ -530,7 +531,8 @@ def process_inbox_request(request_json, activitypublog_id):
nsfl=community.nsfl, nsfl=community.nsfl,
ap_id=request_json['object']['object']['id'], ap_id=request_json['object']['object']['id'],
ap_create_id=request_json['object']['id'], ap_create_id=request_json['object']['id'],
ap_announce_id=request_json['id']) ap_announce_id=request_json['id'],
instance_id=user.instance_id)
if 'source' in request_json['object']['object'] and \ if 'source' in request_json['object']['object'] and \
request_json['object']['object']['source']['mediaType'] == 'text/markdown': request_json['object']['object']['source']['mediaType'] == 'text/markdown':
post_reply.body = request_json['object']['object']['source']['content'] post_reply.body = request_json['object']['object']['source']['content']

View file

@ -56,7 +56,7 @@ def register(app):
db.create_all() db.create_all()
private_key, public_key = RsaKeys.generate_keypair() private_key, public_key = RsaKeys.generate_keypair()
db.session.add(Site(name="PieFed", description='', public_key=public_key, private_key=private_key)) db.session.add(Site(name="PieFed", description='', public_key=public_key, private_key=private_key))
db.session.add(Instance(domain=app.config['SERVER_NAME'], software='PieFed')) db.session.add(Instance(domain=app.config['SERVER_NAME'], software='PieFed')) # Instance 1 is always the local instance
db.session.add(Settings(name='allow_nsfw', value=json.dumps(False))) db.session.add(Settings(name='allow_nsfw', value=json.dumps(False)))
db.session.add(Settings(name='allow_nsfl', value=json.dumps(False))) db.session.add(Settings(name='allow_nsfl', value=json.dumps(False)))
db.session.add(Settings(name='allow_dislike', value=json.dumps(True))) db.session.add(Settings(name='allow_dislike', value=json.dumps(True)))

View file

@ -590,6 +590,7 @@ class PostReply(db.Model):
parent_id = db.Column(db.Integer) parent_id = db.Column(db.Integer)
root_id = db.Column(db.Integer) root_id = db.Column(db.Integer)
depth = db.Column(db.Integer, default=0) depth = db.Column(db.Integer, default=0)
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
body = db.Column(db.Text) body = db.Column(db.Text)
body_html = db.Column(db.Text) body_html = db.Column(db.Text)
body_html_safe = db.Column(db.Boolean, default=False) body_html_safe = db.Column(db.Boolean, default=False)
@ -738,6 +739,7 @@ class Instance(db.Model):
updated_at = db.Column(db.DateTime, default=utcnow) updated_at = db.Column(db.DateTime, default=utcnow)
posts = db.relationship('Post', backref='instance', lazy='dynamic') posts = db.relationship('Post', backref='instance', lazy='dynamic')
post_replies = db.relationship('PostReply', backref='instance', lazy='dynamic')
communities = db.relationship('Community', backref='instance', lazy='dynamic') communities = db.relationship('Community', backref='instance', lazy='dynamic')
@ -850,7 +852,6 @@ class Report(db.Model):
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id')) suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id')) suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
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_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
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)

View file

@ -450,6 +450,13 @@ def post_options(post_id: int):
return render_template('post/post_options.html', post=post) return render_template('post/post_options.html', post=post)
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/options', methods=['GET'])
def post_reply_options(post_id: int, comment_id: int):
post = Post.query.get_or_404(post_id)
post_reply = PostReply.query.get_or_404(comment_id)
return render_template('post/post_reply_options.html', post=post, post_reply=post_reply)
@bp.route('/post/<int:post_id>/edit', methods=['GET', 'POST']) @bp.route('/post/<int:post_id>/edit', methods=['GET', 'POST'])
@login_required @login_required
def post_edit(post_id: int): def post_edit(post_id: int):
@ -471,6 +478,7 @@ def post_edit(post_id: int):
db.session.commit() db.session.commit()
post.flush_cache() post.flush_cache()
flash(_('Your changes have been saved.'), 'success') flash(_('Your changes have been saved.'), 'success')
# todo: federate edit
return redirect(url_for('activitypub.post_ap', post_id=post.id)) return redirect(url_for('activitypub.post_ap', post_id=post.id))
else: else:
if post.type == constants.POST_TYPE_ARTICLE: if post.type == constants.POST_TYPE_ARTICLE:
@ -513,7 +521,8 @@ def post_report(post_id: int):
form = ReportPostForm() form = ReportPostForm()
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_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)
db.session.add(report) db.session.add(report)
# Notify moderators # Notify moderators
@ -591,3 +600,118 @@ def post_mea_culpa(post_id: int):
return redirect(url_for('activitypub.post_ap', post_id=post.id)) return redirect(url_for('activitypub.post_ap', post_id=post.id))
return render_template('post/post_mea_culpa.html', title=_('I changed my mind'), form=form, post=post) return render_template('post/post_mea_culpa.html', title=_('I changed my mind'), form=form, post=post)
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/report', methods=['GET', 'POST'])
@login_required
def post_reply_report(post_id: int, comment_id: int):
post = Post.query.get_or_404(post_id)
post_reply = PostReply.query.get_or_404(comment_id)
form = ReportPostForm()
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_post_id=post.id, suspect_community_id=post.community.id,
suspect_user_id=post_reply.author.id, suspect_post_reply_id=post_reply.id)
db.session.add(report)
# Notify moderators
for mod in post.community.moderators():
notification = Notification(user_id=mod.user_id, title=_('A comment has been reported'),
url=f"https://{current_app.config['SERVER_NAME']}/comment/{post_reply.id}",
author_id=current_user.id)
db.session.add(notification)
post_reply.reports += 1
# todo: Also notify admins for certain types of report
db.session.commit()
# todo: federate report to originating instance
if not post.community.is_local() and form.report_remote.data:
...
flash(_('Comment has been reported, thank you!'))
return redirect(post.community.local_url())
elif request.method == 'GET':
form.report_remote.data = True
return render_template('post/post_reply_report.html', title=_('Report comment'), form=form, post=post, post_reply=post_reply)
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/block_user', methods=['GET', 'POST'])
@login_required
def post_reply_block_user(post_id: int, comment_id: int):
post = Post.query.get_or_404(post_id)
post_reply = PostReply.query.get_or_404(comment_id)
existing = UserBlock.query.filter_by(blocker_id=current_user.id, blocked_id=post_reply.author.id).first()
if not existing:
db.session.add(UserBlock(blocker_id=current_user.id, blocked_id=post_reply.author.id))
db.session.commit()
flash(_('%(name)s has been blocked.', name=post_reply.author.user_name))
# todo: federate block to post_reply author instance
return redirect(url_for('activitypub.post_ap', post_id=post.id))
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/block_instance', methods=['GET', 'POST'])
@login_required
def post_reply_block_instance(post_id: int, comment_id: int):
post = Post.query.get_or_404(post_id)
post_reply = PostReply.query.get_or_404(comment_id)
existing = InstanceBlock.query.filter_by(user_id=current_user.id, instance_id=post_reply.instance_id).first()
if not existing:
db.session.add(InstanceBlock(user_id=current_user.id, instance_id=post_reply.instance_id))
db.session.commit()
flash(_('Content from %(name)s will be hidden.', name=post_reply.instance.domain))
return redirect(url_for('activitypub.post_ap', post_id=post.id))
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/edit', methods=['GET', 'POST'])
@login_required
def post_reply_edit(post_id: int, comment_id: int):
post = Post.query.get_or_404(post_id)
post_reply = PostReply.query.get_or_404(comment_id)
if post_reply.parent_id:
comment = PostReply.query.get_or_404(post_reply.parent_id)
else:
comment = None
form = NewReplyForm()
if post_reply.user_id == current_user.id or post.community.is_moderator():
if form.validate_on_submit():
post_reply.body = form.body.data
post_reply.body_html = markdown_to_html(form.body.data)
post_reply.notify_author = form.notify_author.data
post.community.last_active = utcnow()
post_reply.edited_at = utcnow()
db.session.commit()
post.flush_cache()
flash(_('Your changes have been saved.'), 'success')
# todo: federate edit
return redirect(url_for('activitypub.post_ap', post_id=post.id))
else:
form.body.data = post_reply.body
form.notify_author.data = not post_reply.notify_author
return render_template('post/post_reply_edit.html', title=_('Edit comment'), form=form, post=post, post_reply=post_reply,
comment=comment, markdown_editor=True)
else:
abort(401)
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/delete', methods=['GET', 'POST'])
@login_required
def post_reply_delete(post_id: int, comment_id: int):
post = Post.query.get_or_404(post_id)
post_reply = PostReply.query.get_or_404(comment_id)
community = post.community
if post_reply.user_id == current_user.id or community.is_moderator():
if post_reply.has_replies():
post_reply.body = 'Deleted by author' if post_reply.author.id == current_user.id else 'Deleted by moderator'
post_reply.body_html = markdown_to_html(post_reply.body)
else:
post_reply.delete_dependencies()
db.session.delete(post_reply)
g.site.last_active = community.last_active = utcnow()
db.session.commit()
post.flush_cache()
flash(_('Comment deleted.'))
# todo: federate delete
return redirect(url_for('activitypub.post_ap', post_id=post.id))

View file

@ -618,6 +618,7 @@ fieldset legend {
} }
.comment .comment_actions { .comment .comment_actions {
margin-top: -10px; margin-top: -10px;
position: relative;
} }
.comment .comment_actions a { .comment .comment_actions a {
text-decoration: none; text-decoration: none;

View file

@ -337,6 +337,7 @@ nav, etc which are used site-wide */
.comment_actions { .comment_actions {
margin-top: -10px; margin-top: -10px;
position: relative;
a { a {
text-decoration: none; text-decoration: none;
} }

View file

@ -545,6 +545,15 @@ nav.navbar {
text-decoration: none; text-decoration: none;
} }
.comment_actions_link {
display: block;
position: absolute;
bottom: 0;
right: 48px;
width: 41px;
text-decoration: none;
}
.alert { .alert {
width: 96%; width: 96%;
} }

View file

@ -241,6 +241,15 @@ nav.navbar {
text-decoration: none; text-decoration: none;
} }
.comment_actions_link {
display: block;
position: absolute;
bottom: 0;
right: 48px;
width: 41px;
text-decoration: none;
}
.alert { .alert {
width: 96%; width: 96%;
} }

View file

@ -89,13 +89,14 @@
{{ comment['comment'].body_html | safe }} {{ comment['comment'].body_html | safe }}
</div> </div>
</div> </div>
{% if current_user.is_authenticated and current_user.verified %} <div class="comment_actions hidable">
<div class="comment_actions hidable"> {% if current_user.is_authenticated and current_user.verified %}
{% if post.comments_enabled %} {% if post.comments_enabled %}
<a href="{{ url_for('post.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a> <a href="{{ url_for('post.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a>
{% endif %} {% endif %}
</div> {% endif %}
{% endif %} <a href="{{ url_for('post.post_reply_options', post_id=post.id, comment_id=comment['comment'].id) }}" class="comment_actions_link" rel="nofollow"><span class="fe fe-options" title="Options"> </span></a>
</div>
{% if comment['replies'] %} {% if comment['replies'] %}
{% if comment['comment'].depth <= THREAD_CUTOFF_DEPTH %} {% if comment['comment'].depth <= THREAD_CUTOFF_DEPTH %}
<div class="replies hidable"> <div class="replies hidable">

View file

@ -26,16 +26,16 @@
<li><a href="{{ url_for('post.post_block_domain', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span> <li><a href="{{ url_for('post.post_block_domain', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _('Block domain %(domain)s', domain=post.domain.name) }}</a></li> {{ _('Block domain %(domain)s', domain=post.domain.name) }}</a></li>
{% endif %} {% endif %}
{% if post.instance_id %} {% if post.instance_id and post.instance_id != 1 %}
<li><a href="{{ url_for('post.post_block_instance', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span> <li><a href="{{ url_for('post.post_block_instance', post_id=post.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _('Hide every post from %(name)s', name=post.instance.domain) }}</a></li> {{ _("Hide every post from author's instance: %(name)s", name=post.instance.domain) }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline"><span class="fe fe-report"></span> <li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline"><span class="fe fe-report"></span>
{{ _('Report to moderators') }}</a></li> {{ _('Report to moderators') }}</a></li>
{% endif %} {% endif %}
</ul> </ul>
<p>{{ _('If you want to perform more than one of these (e.g. block and report), hold down Ctrl and click, then complete the operation in the new tabs that open.') }}</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,85 @@
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %}
{% block app_content %}
<script src="/static/js/coolfieldset.js"></script>
<div class="row">
<div class="col-12 col-md-8 position-relative add_reply main_pane">
<fieldset class="coolfieldset mt-4"><legend class="w-auto">Original post</legend>
<h3>{{ post.title }}</h3>
{{ post.body_html | safe }}
</fieldset>
{% if comment %}
<fieldset class="coolfieldset mt-4"><legend class="w-auto">Comment you are replying to</legend>
{{ comment.body_html | safe}}
</fieldset>
{% endif %}
<div class="position-relative">
{{ render_form(form) }}
{% if markdown_editor %}
<script nonce="{{ session['nonce'] }}">
window.addEventListener("load", function () {
var downarea = new DownArea({
elem: document.querySelector('#body'),
resize: DownArea.RESIZE_VERTICAL,
hide: ['heading', 'bold-italic'],
value: {{ form.body.data | tojson | safe }}
});
setupAutoResize('body');
});
</script>
{% endif %}
</div>
</div>
<div class="col-12 col-md-4">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-6">
{% if current_user.is_authenticated and community_membership(current_user, post.community) %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
{% else %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
{% endif %}
</div>
<div class="col-6">
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create post') }}</a>
</div>
</div>
<form method="get">
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
</form>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h2>{{ _('About community') }}</h2>
</div>
<div class="card-body">
<p>{{ post.community.description|safe }}</p>
<p>{{ post.community.rules|safe }}</p>
{% if len(mods) > 0 and not post.community.private_mods %}
<h3>Moderators</h3>
<ol>
{% for mod in mods %}
<li><a href="/u/{{ mod.user_name }}">{{ mod.user_name }}</a></li>
{% endfor %}
</ol>
{% endif %}
</div>
</div>
{% if is_moderator %}
<div class="card mt-3">
<div class="card-header">
<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>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% 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">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
<ul class="option_list">
{% if current_user.is_authenticated %}
{% if post_reply.user_id == current_user.id or post.community.is_moderator() %}
<li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
{{ _('Edit') }}</a></li>
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li>
{% endif %}
{% if post_reply.user_id != current_user.id %}
<li><a href="{{ url_for('post.post_reply_block_user', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _('Block author @%(author_name)s', author_name=post_reply.author.user_name) }}</a></li>
{% if post_reply.instance_id and post_reply.instance_id != 1 %}
<li><a href="{{ url_for('post.post_reply_block_instance', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-block"></span>
{{ _("Hide every post from author's instance: %(name)s", name=post_reply.instance.domain) }}</a></li>
{% endif %}
{% endif %}
<li><a href="{{ url_for('post.post_reply_report', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline"><span class="fe fe-report"></span>
{{ _('Report to moderators') }}</a></li>
{% endif %}
</ul>
<p>{{ _('If you want to perform more than one of these (e.g. block and report), hold down Ctrl and click, then complete the operation in the new tabs that open.') }}</p>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% 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">{{ _('Report comment on "%(post_title)s" by %(reply_name)s',
post_title=post.title, reply_name=post_reply.author.user_name) }}</div>
<div class="card-body">
{{ render_form(form) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,36 @@
"""post reply instance
Revision ID: e8113bc01e3a
Revises: 52789c4b1d0f
Create Date: 2023-12-28 13:11:04.462308
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e8113bc01e3a'
down_revision = '52789c4b1d0f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('post_reply', schema=None) as batch_op:
batch_op.add_column(sa.Column('instance_id', sa.Integer(), nullable=True))
batch_op.create_index(batch_op.f('ix_post_reply_instance_id'), ['instance_id'], unique=False)
batch_op.create_foreign_key(None, 'instance', ['instance_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('post_reply', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_index(batch_op.f('ix_post_reply_instance_id'))
batch_op.drop_column('instance_id')
# ### end Alembic commands ###

2
regional.txt Normal file
View file

@ -0,0 +1,2 @@
africa
https://baraza.africa/c/africa