From b431a79518600580e527ff058e66054cc498471e Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Wed, 3 Jan 2024 20:14:39 +1300 Subject: [PATCH] sort posts by top, hot, new --- app/activitypub/routes.py | 2 +- app/activitypub/util.py | 9 ++- app/community/routes.py | 18 +++-- app/community/util.py | 5 +- app/main/routes.py | 89 +++++++++++++++++++-- app/post/routes.py | 11 +-- app/templates/_home_nav.html | 11 +++ app/templates/admin/activities.html | 2 +- app/templates/community/_community_nav.html | 11 +++ app/templates/community/community.html | 5 +- app/templates/index.html | 6 +- app/templates/list_communities.html | 3 - app/templates/new_posts.html | 71 ++++++++++++++++ app/templates/top_posts.html | 71 ++++++++++++++++ app/utils.py | 13 +++ config.py | 1 - 16 files changed, 298 insertions(+), 30 deletions(-) create mode 100644 app/templates/_home_nav.html create mode 100644 app/templates/community/_community_nav.html create mode 100644 app/templates/new_posts.html create mode 100644 app/templates/top_posts.html diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 52277978..3b6523ae 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -701,7 +701,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address): user.instance.ip_address = ip_address user.instance.dormant = False if 'community' in vars() and community is not None: - if community.is_local() and request_json['type'] not in ['Announce', 'Follow', 'Accept']: + if community.is_local() and request_json['type'] not in ['Announce', 'Follow', 'Accept', 'Undo']: announce_activity_to_followers(community, user, request_json) # community.flush_cache() if 'post' in vars() and post is not None: diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 08e28c4d..8191cb93 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -20,7 +20,7 @@ from PIL import Image, ImageOps from io import BytesIO from app.utils import get_request, allowlist_html, html_to_markdown, get_setting, ap_datetime, markdown_to_html, \ - is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request + is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request, post_ranking def public_key(): @@ -729,6 +729,8 @@ def downvote_post(post, user): db.session.add(vote) else: pass # they have already downvoted this post + post.ranking = post_ranking(post.score, post.posted_at) + db.session.commit() def downvote_post_reply(comment, user): @@ -831,6 +833,8 @@ def upvote_post(post, user): effect = 0 post.author.reputation += effect db.session.add(vote) + post.ranking = post_ranking(post.score, post.posted_at) + db.session.commit() def delete_post_or_comment(user_ap_id, community_ap_id, to_be_deleted_ap_id): @@ -972,6 +976,7 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json post.image = image if post is not None: db.session.add(post) + post.ranking = post_ranking(post.score, post.posted_at) community.post_count += 1 community.last_active = utcnow() activity_log.result = 'success' @@ -986,7 +991,7 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json db.session.add(vote) post.up_votes += 1 post.score += 1 - post.ranking += 1 + post.ranking = post_ranking(post.score, post.posted_at) db.session.commit() return post diff --git a/app/community/routes.py b/app/community/routes.py index df3c08e9..c4535e96 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -19,7 +19,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_ shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \ request_etag_matches, return_304, instance_banned, can_create, can_upvote, can_downvote from feedgen.feed import FeedGenerator -from datetime import timezone +from datetime import timezone, timedelta @bp.route('/add_local', methods=['GET', 'POST']) @@ -98,6 +98,7 @@ def show_community(community: Community): abort(404) page = request.args.get('page', 1, type=int) + sort = request.args.get('sort', '') mods = community.moderators() @@ -113,17 +114,24 @@ def show_community(community: Community): mod_list = User.query.filter(User.id.in_(mod_user_ids)).all() if current_user.is_anonymous or current_user.ignore_bots: - posts = community.posts.filter(Post.from_bot == False).order_by(desc(Post.last_active)).paginate(page=page, per_page=100, error_out=False) + posts = community.posts.filter(Post.from_bot == False) else: - posts = community.posts.order_by(desc(Post.last_active)).paginate(page=page, per_page=100, error_out=False) + posts = community.posts + if sort == '' or sort == 'hot': + posts = posts.order_by(desc(Post.ranking)) + elif sort == 'top': + posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.score)) + elif sort == 'new': + posts = posts.order_by(desc(Post.posted_at)) + posts = posts.paginate(page=page, per_page=100, error_out=False) description = shorten_string(community.description, 150) if community.description else None og_image = community.image.source_url if community.image_id else None next_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name, - page=posts.next_num) if posts.has_next else None + page=posts.next_num, sort=sort) if posts.has_next else None prev_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name, - page=posts.prev_num) if posts.has_prev and page != 1 else None + page=posts.prev_num, sort=sort) if posts.has_prev and page != 1 else None return render_template('community/community.html', community=community, title=community.title, is_moderator=is_moderator, is_owner=is_owner, is_admin=is_admin, mods=mod_list, posts=posts, description=description, diff --git a/app/community/util.py b/app/community/util.py index 4ad58264..7cf2be9c 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -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 from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, validate_image, allowlist_html, \ - html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain + html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking from sqlalchemy import desc, text import os from opengraph_parse import parse_page @@ -103,6 +103,7 @@ def retrieve_mods_and_backfill(community_id: int): post = post_json_to_model(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() activities_processed += 1 @@ -267,7 +268,9 @@ def save_post(form, post: Post): post.score = -1 post.ranking = -1 db.session.add(postvote) + post.ranking = post_ranking(post.score, utcnow()) db.session.add(post) + g.site.last_active = utcnow() diff --git a/app/main/routes.py b/app/main/routes.py index d46efc5f..0a6d34e3 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,3 +1,6 @@ +from datetime import datetime, timedelta +from math import log + from sqlalchemy.sql.operators import or_ from app import db, cache @@ -12,7 +15,7 @@ from sqlalchemy import select, desc from sqlalchemy_searchable import search from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \ ap_datetime, ip_address -from app.models import Community, CommunityMember, Post, Site, User +from app.models import Community, CommunityMember, Post, Site, User, utcnow @bp.route('/', methods=['HEAD', 'GET', 'POST']) @@ -40,7 +43,7 @@ def index(): if domains_ids: posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None)) - posts = posts.order_by(desc(Post.last_active)).paginate(page=page, per_page=100, error_out=False) + posts = posts.order_by(desc(Post.ranking)).paginate(page=page, per_page=100, error_out=False) next_url = url_for('main.index', page=posts.next_num) if posts.has_next else None prev_url = url_for('main.index', page=posts.prev_num) if posts.has_prev and page != 1 else None @@ -54,6 +57,78 @@ def index(): rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", rss_feed_name=f"Posts on " + g.site.name) +@bp.route('/new', methods=['HEAD', 'GET', 'POST']) +def new_posts(): + verification_warning() + + # If nothing has changed since their last visit, return HTTP 304 + current_etag = f"new_{hash(str(g.site.last_active))}" + if current_user.is_anonymous and request_etag_matches(current_etag): + return return_304(current_etag) + + page = request.args.get('page', 1, type=int) + + if current_user.is_anonymous: + posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False) + else: + posts = Post.query.join(CommunityMember, Post.community_id == CommunityMember.community_id).filter( + CommunityMember.is_banned == False) + posts = posts.join(User, CommunityMember.user_id == User.id).filter(User.id == current_user.id) + domains_ids = blocked_domains(current_user.id) + if domains_ids: + posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None)) + + posts = posts.order_by(desc(Post.posted_at)).paginate(page=page, per_page=100, error_out=False) + + next_url = url_for('main.new_posts', page=posts.next_num) if posts.has_next else None + prev_url = url_for('main.new_posts', page=posts.prev_num) if posts.has_prev and page != 1 else None + + active_communities = Community.query.filter_by(banned=False).order_by(desc(Community.last_active)).limit(5).all() + + return render_template('new_posts.html', posts=posts, active_communities=active_communities, + POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, + SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, + etag=f"home_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url, + rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", + rss_feed_name=f"Posts on " + g.site.name) + + +@bp.route('/top', methods=['HEAD', 'GET', 'POST']) +def top_posts(): + verification_warning() + + # If nothing has changed since their last visit, return HTTP 304 + current_etag = f"best_{hash(str(g.site.last_active))}" + if current_user.is_anonymous and request_etag_matches(current_etag): + return return_304(current_etag) + + page = request.args.get('page', 1, type=int) + + if current_user.is_anonymous: + posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False) + else: + posts = Post.query.join(CommunityMember, Post.community_id == CommunityMember.community_id).filter( + CommunityMember.is_banned == False) + posts = posts.join(User, CommunityMember.user_id == User.id).filter(User.id == current_user.id) + domains_ids = blocked_domains(current_user.id) + if domains_ids: + posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None)) + + posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.score)).paginate(page=page, per_page=100, error_out=False) + + next_url = url_for('main.top_posts', page=posts.next_num) if posts.has_next else None + prev_url = url_for('main.top_posts', page=posts.prev_num) if posts.has_prev and page != 1 else None + + active_communities = Community.query.filter_by(banned=False).order_by(desc(Community.last_active)).limit(5).all() + + return render_template('top_posts.html', posts=posts, active_communities=active_communities, + POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, + SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, + etag=f"home_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url, + rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", + rss_feed_name=f"Posts on " + g.site.name) + + @bp.route('/communities', methods=['GET']) def list_communities(): verification_warning() @@ -99,10 +174,12 @@ def robots(): @bp.route('/test') def test(): - ip = request.headers.get('X-Forwarded-For') or request.remote_addr - if ',' in ip: # Remove all but first ip addresses - ip = ip[:ip.index(',')].strip() - return ip + return 'done' + + #ip = request.headers.get('X-Forwarded-For') or request.remote_addr + #if ',' in ip: # Remove all but first ip addresses + # ip = ip[:ip.index(',')].strip() + #return ip def verification_warning(): diff --git a/app/post/routes.py b/app/post/routes.py index 584d3ce9..c8485b31 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -18,7 +18,7 @@ from app.models import Post, PostReply, \ from app.post import bp from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \ shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \ - request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote + request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking def show_post(post_id: int): @@ -155,11 +155,11 @@ def show_post(post_id: int): og_image = post.image.source_url if post.image_id else None description = shorten_string(markdown_to_text(post.body), 150) if post.body else None - return render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, + return render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community, canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH, description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE, POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE, - etag=f"{post.id}_{hash(post.last_active)}", markdown_editor=True, community=community) + etag=f"{post.id}_{hash(post.last_active)}", markdown_editor=True) @bp.route('/post//', methods=['GET', 'POST']) @@ -247,11 +247,12 @@ def post_vote(post_id: int, vote_direction): current_user.last_seen = utcnow() current_user.ip_address = ip_address() if not current_user.banned: + post.ranking = post_ranking(post.score, post.created_at) db.session.commit() current_user.recalculate_attitude() db.session.commit() post.flush_cache() - return render_template('post/_post_voting_buttons.html', post=post, + return render_template('post/_post_voting_buttons.html', post=post, community=post.community, upvoted_class=upvoted_class, downvoted_class=downvoted_class) @@ -327,7 +328,7 @@ def comment_vote(comment_id, vote_direction): comment.post.flush_cache() return render_template('post/_voting_buttons.html', comment=comment, upvoted_class=upvoted_class, - downvoted_class=downvoted_class) + downvoted_class=downvoted_class, community=comment.community) @bp.route('/post//comment/') diff --git a/app/templates/_home_nav.html b/app/templates/_home_nav.html new file mode 100644 index 00000000..976eb946 --- /dev/null +++ b/app/templates/_home_nav.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/app/templates/admin/activities.html b/app/templates/admin/activities.html index e88414fb..65737e7f 100644 --- a/app/templates/admin/activities.html +++ b/app/templates/admin/activities.html @@ -40,7 +40,7 @@

{{ community.title }}

{% endif %} + {% include "community/_community_nav.html" %}
{% for post in posts %} {% include 'post/_post_teaser.html' %} @@ -45,12 +46,12 @@