mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-02-03 00:31:25 -08:00
Merge pull request 'Cross Posts' (#144) from freamon/pyfedi:xp3 into main
Reviewed-on: https://codeberg.org/rimu/pyfedi/pulls/144
This commit is contained in:
commit
bf03020089
12 changed files with 211 additions and 5 deletions
|
@ -629,6 +629,13 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
activity_log.result = 'failure'
|
||||
activity_log.exception_message = 'dict instead of string ' + str(to_be_deleted_ap_id)
|
||||
else:
|
||||
post = Post.query.filter_by(ap_id=to_be_deleted_ap_id).first()
|
||||
if post and post.url and post.cross_posts is not None:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
delete_post_or_comment(user_ap_id, community_ap_id, to_be_deleted_ap_id)
|
||||
activity_log.result = 'success'
|
||||
elif request_json['object']['type'] == 'Page': # Editing a post
|
||||
|
@ -858,6 +865,12 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
post = Post.query.filter_by(ap_id=ap_id).first()
|
||||
# Delete post
|
||||
if post:
|
||||
if post.url and post.cross_posts is not None:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
post.delete_dependencies()
|
||||
post.community.post_count -= 1
|
||||
db.session.delete(post)
|
||||
|
|
|
@ -1397,6 +1397,20 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
|||
if post.image_id:
|
||||
make_image_sizes(post.image_id, 150, 512, 'posts') # the 512 sized image is for masonry view
|
||||
|
||||
if post.url:
|
||||
other_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.posted_at - timedelta(days=6)).all()
|
||||
for op in other_posts:
|
||||
if op.cross_posts is None:
|
||||
op.cross_posts = [post.id]
|
||||
else:
|
||||
op.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [op.id]
|
||||
else:
|
||||
post.cross_posts.append(op.id)
|
||||
db.session.commit()
|
||||
|
||||
notify_about_post(post)
|
||||
|
||||
if user.reputation > 100:
|
||||
|
@ -1503,6 +1517,27 @@ def update_post_from_activity(post: Post, request_json: dict):
|
|||
post.domain = domain
|
||||
else:
|
||||
post.url = old_url # don't change if url changed from non-banned domain to banned domain
|
||||
|
||||
new_cross_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > utcnow() - timedelta(days=6)).all()
|
||||
for ncp in new_cross_posts:
|
||||
if ncp.cross_posts is None:
|
||||
ncp.cross_posts = [post.id]
|
||||
else:
|
||||
ncp.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [ncp.id]
|
||||
else:
|
||||
post.cross_posts.append(ncp.id)
|
||||
|
||||
if post.url != old_url:
|
||||
if post.cross_posts is not None:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
|
||||
if post is not None:
|
||||
if 'image' in request_json['object'] and post.image is None:
|
||||
image = File(source_url=request_json['object']['image']['url'])
|
||||
|
|
|
@ -470,6 +470,20 @@ def add_post(actor):
|
|||
if post.image_id and post.image.file_path is None:
|
||||
make_image_sizes(post.image_id, 150, 512, 'posts') # the 512 sized image is for masonry view
|
||||
|
||||
if post.url:
|
||||
other_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.posted_at - timedelta(days=6)).all()
|
||||
for op in other_posts:
|
||||
if op.cross_posts is None:
|
||||
op.cross_posts = [post.id]
|
||||
else:
|
||||
op.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [op.id]
|
||||
else:
|
||||
post.cross_posts.append(op.id)
|
||||
db.session.commit()
|
||||
|
||||
notify_about_post(post)
|
||||
|
||||
if not community.local_only:
|
||||
|
|
|
@ -106,10 +106,23 @@ def retrieve_mods_and_backfill(community_id: int):
|
|||
db.session.add(activity_log)
|
||||
if user:
|
||||
post = post_json_to_model(activity_log, activity['object']['object'], user, community)
|
||||
post.ap_create_id = activity['object']['id']
|
||||
post.ap_announce_id = activity['id']
|
||||
post.ranking = post_ranking(post.score, post.posted_at)
|
||||
db.session.commit()
|
||||
if post:
|
||||
post.ap_create_id = activity['object']['id']
|
||||
post.ap_announce_id = activity['id']
|
||||
post.ranking = post_ranking(post.score, post.posted_at)
|
||||
if post.url:
|
||||
other_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.posted_at - timedelta(days=3), Post.posted_at < post.posted_at + timedelta(days=3)).all()
|
||||
for op in other_posts:
|
||||
if op.cross_posts is None:
|
||||
op.cross_posts = [post.id]
|
||||
else:
|
||||
op.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [op.id]
|
||||
else:
|
||||
post.cross_posts.append(op.id)
|
||||
db.session.commit()
|
||||
else:
|
||||
activity_log.exception_message = 'Could not find or create actor'
|
||||
db.session.commit()
|
||||
|
|
|
@ -10,6 +10,8 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
|||
from flask_babel import _, lazy_gettext as _l
|
||||
from sqlalchemy.orm import backref
|
||||
from sqlalchemy_utils.types import TSVectorType # https://sqlalchemy-searchable.readthedocs.io/en/latest/installation.html
|
||||
from sqlalchemy.dialects.postgresql import ARRAY
|
||||
from sqlalchemy.ext.mutable import MutableList
|
||||
from flask_sqlalchemy import BaseQuery
|
||||
from sqlalchemy_searchable import SearchQueryMixin
|
||||
from app import db, login, cache, celery
|
||||
|
@ -869,6 +871,7 @@ class Post(db.Model):
|
|||
language = db.Column(db.String(10))
|
||||
edited_at = db.Column(db.DateTime)
|
||||
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
|
||||
cross_posts = db.Column(MutableList.as_mutable(ARRAY(db.Integer)))
|
||||
|
||||
ap_id = db.Column(db.String(255), index=True)
|
||||
ap_create_id = db.Column(db.String(100))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from random import randint
|
||||
|
||||
from flask import redirect, url_for, flash, current_app, abort, request, g, make_response
|
||||
|
@ -683,12 +683,36 @@ def post_edit(post_id: int):
|
|||
form.nsfw.render_kw = {'disabled': True}
|
||||
|
||||
#form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
old_url = post.url
|
||||
|
||||
if form.validate_on_submit():
|
||||
save_post(form, post)
|
||||
post.community.last_active = utcnow()
|
||||
post.edited_at = utcnow()
|
||||
db.session.commit()
|
||||
|
||||
if post.url != old_url:
|
||||
if post.cross_posts is not None:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
|
||||
new_cross_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.edited_at - timedelta(days=6)).all()
|
||||
for ncp in new_cross_posts:
|
||||
if ncp.cross_posts is None:
|
||||
ncp.cross_posts = [post.id]
|
||||
else:
|
||||
ncp.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [ncp.id]
|
||||
else:
|
||||
post.cross_posts.append(ncp.id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
post.flush_cache()
|
||||
flash(_('Your changes have been saved.'), 'success')
|
||||
# federate edit
|
||||
|
@ -807,6 +831,13 @@ def post_delete(post_id: int):
|
|||
post = Post.query.get_or_404(post_id)
|
||||
community = post.community
|
||||
if post.user_id == current_user.id or community.is_moderator() or current_user.is_admin():
|
||||
if post.url:
|
||||
if post.cross_posts is not None:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
post.delete_dependencies()
|
||||
post.flush_cache()
|
||||
db.session.delete(post)
|
||||
|
@ -1241,3 +1272,10 @@ def post_reply_notification(post_reply_id: int):
|
|||
post_reply.notify_author = not post_reply.notify_author
|
||||
db.session.commit()
|
||||
return render_template('post/_reply_notification_toggle.html', comment={'comment': post_reply})
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/cross_posts', methods=['GET'])
|
||||
def post_cross_posts(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
return render_template('post/post_cross_posts.html', post=post, cross_posts=cross_posts)
|
||||
|
|
|
@ -174,6 +174,15 @@
|
|||
content: "\e990";
|
||||
}
|
||||
|
||||
.fe-layers {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.fe-layers::before {
|
||||
content: "\e97f";
|
||||
}
|
||||
|
||||
.fe-report::before {
|
||||
content: "\e967";
|
||||
}
|
||||
|
|
|
@ -201,6 +201,15 @@ nav, etc which are used site-wide */
|
|||
content: "\e990";
|
||||
}
|
||||
|
||||
.fe-layers {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.fe-layers::before {
|
||||
content: "\e97f";
|
||||
}
|
||||
|
||||
.fe-report::before {
|
||||
content: "\e967";
|
||||
}
|
||||
|
|
|
@ -200,6 +200,15 @@
|
|||
content: "\e990";
|
||||
}
|
||||
|
||||
.fe-layers {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.fe-layers::before {
|
||||
content: "\e97f";
|
||||
}
|
||||
|
||||
.fe-report::before {
|
||||
content: "\e967";
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@
|
|||
{% if post.nsfw %}<span class="warning_badge nsfw" title="{{ _('Not safe for work') }}">nsfw</span>{% endif %}
|
||||
{% if post.nsfl %}<span class="warning_badge nsfl" title="{{ _('Potentially emotionally scarring content') }}">nsfl</span>{% endif %}
|
||||
</h1>
|
||||
{% if post.cross_posts %}
|
||||
<a href="{{ url_for('post.post_cross_posts', post_id=post.id) }}" aria-label="{{ _('Show cross-posts') }}"><span class="fe fe-layers"></span></a>
|
||||
<span aria-label="{{ _('Number of cross-posts:') }}">{{ len(post.cross_posts) }}</span>
|
||||
{% endif %}
|
||||
{% if post.url %}
|
||||
<p><a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="Go to image">{{ post.url|shorten_url }}
|
||||
<span class="fe fe-external"></span></a></p>
|
||||
|
@ -66,6 +70,10 @@
|
|||
{% if post.nsfw %}<span class="warning_badge nsfw" title="{{ _('Not safe for work') }}">nsfw</span>{% endif %}
|
||||
{% if post.nsfl %}<span class="warning_badge nsfl" title="{{ _('Potentially emotionally scarring content') }}">nsfl</span>{% endif %}
|
||||
</h1>
|
||||
{% if post.cross_posts %}
|
||||
<a href="{{ url_for('post.post_cross_posts', post_id=post.id) }}" aria-label="{{ _('Show cross-posts') }}"><span class="fe fe-layers"></span></a>
|
||||
<span aria-label="{{ _('Number of cross-posts:') }}">{{ len(post.cross_posts) }}</span>
|
||||
{% endif %}
|
||||
{% if post.type == POST_TYPE_LINK and post.image_id and not (post.url and 'youtube.com' in post.url) %}
|
||||
<div class="url_thumbnail">
|
||||
<a href="{{ post.url }}" target="_blank" rel="nofollow ugc" class="post_link"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text if post.image.alt_text else '' }}"
|
||||
|
|
23
app/templates/post/post_cross_posts.html
Normal file
23
app/templates/post/post_cross_posts.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% 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">
|
||||
<h3 class="mt-2 post_title">{{ _('Cross-posts for "%(post_title)s"', post_title=post.title) }}</h3>
|
||||
<ul class="cross_post_list">
|
||||
{% for cross_post in cross_posts %}
|
||||
<li><a href="{{ url_for('activitypub.post_ap', post_id=cross_post.id) }}">
|
||||
{{ cross_post.community.title }}@{{ cross_post.community.ap_domain }}</a>
|
||||
<span class="fe fe-reply"></span>
|
||||
<span>{{ cross_post.reply_count }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
32
migrations/versions/08b3f718df5d_cross_posts.py
Normal file
32
migrations/versions/08b3f718df5d_cross_posts.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""cross posts
|
||||
|
||||
Revision ID: 08b3f718df5d
|
||||
Revises: 04697ae91fac
|
||||
Create Date: 2024-03-31 02:10:19.726154
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '08b3f718df5d'
|
||||
down_revision = '04697ae91fac'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('cross_posts', postgresql.ARRAY(sa.Integer()), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.drop_column('cross_posts')
|
||||
|
||||
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue