topic notifications #20

This commit is contained in:
rimu 2024-04-29 16:03:00 +12:00
parent 2008ce89a6
commit e1204bc267
7 changed files with 62 additions and 28 deletions

View file

@ -28,7 +28,8 @@ import pytesseract
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, microblog_content_to_title, generate_image_from_video_url, is_video_url, reply_is_stupid
blocked_phrases, microblog_content_to_title, generate_image_from_video_url, is_video_url, reply_is_stupid, \
notification_subscribers, communities_banned_from
def public_key():
@ -1524,7 +1525,8 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
post.cross_posts.append(op.id)
db.session.commit()
notify_about_post(post)
if post.community_id not in communities_banned_from(user.id):
notify_about_post(post)
if user.reputation > 100:
post.up_votes += 1
@ -1537,20 +1539,12 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
def notify_about_post(post: Post):
# todo: eventually this function could trigger a lot of DB activity. This function will need to be a celery task.
# Send notifications based on subscriptions to the author
# Send notifications based on subscriptions
notifications_sent_to = set()
for notify_id in post.author.notification_subscribers():
if notify_id != post.user_id:
new_notification = Notification(title=shorten_string(post.title, 50), url=f"/post/{post.id}",
user_id=notify_id, author_id=post.user_id)
db.session.add(new_notification)
user = User.query.get(notify_id)
user.unread_notifications += 1
db.session.commit()
notifications_sent_to.add(notify_id)
# Send notifications based on subscriptions to the community
for notify_id in post.community.notification_subscribers():
send_notifs_to = set(notification_subscribers(post.author_id, NOTIF_USER) +
notification_subscribers(post.community_id, NOTIF_COMMUNITY) +
notification_subscribers(post.community.topic_id, NOTIF_TOPIC))
for notify_id in send_notifs_to:
if notify_id != post.user_id and notify_id not in notifications_sent_to:
new_notification = Notification(title=shorten_string(post.title, 50), url=f"/post/{post.id}",
user_id=notify_id, author_id=post.user_id)
@ -1558,6 +1552,7 @@ def notify_about_post(post: Post):
user = User.query.get(notify_id)
user.unread_notifications += 1
db.session.commit()
notifications_sent_to.add(notify_id)
def update_post_reply_from_activity(reply: PostReply, request_json: dict):

View file

@ -497,7 +497,8 @@ def add_discussion_post(actor):
db.session.commit()
upvote_own_post(post)
notify_about_post(post)
if not post.community.user_is_banned(current_user):
notify_about_post(post)
if not community.local_only:
federate_post(community, post)

View file

@ -19,7 +19,7 @@ import jwt
import os
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER, \
SUBSCRIPTION_BANNED, SUBSCRIPTION_PENDING, NOTIF_USER, NOTIF_COMMUNITY
SUBSCRIPTION_BANNED, SUBSCRIPTION_PENDING, NOTIF_USER, NOTIF_COMMUNITY, NOTIF_TOPIC
# datetime.utcnow() is depreciated in Python 3.12 so it will need to be swapped out eventually
@ -327,6 +327,14 @@ class Topic(db.Model):
return_value = list(reversed(return_value))
return '/'.join(return_value)
def notify_new_posts(self, user_id: int) -> bool:
existing_notification = NotificationSubscription.query.filter(NotificationSubscription.entity_id == self.id,
NotificationSubscription.user_id == user_id,
NotificationSubscription.type == NOTIF_TOPIC).first()
return existing_notification is not None
class Community(db.Model):
query_class = FullTextSearchQuery
@ -476,14 +484,9 @@ class Community(db.Model):
return False
def user_is_banned(self, user):
membership = CommunityMember.query.filter(CommunityMember.community_id == self.id, CommunityMember.user_id == user.id).first()
if membership and membership.is_banned:
return True
banned = CommunityBan.query.filter(CommunityBan.community_id == self.id, CommunityBan.user_id == user.id).first()
if banned:
return True
return False
# use communities_banned_from() instead of this method, where possible. Redis caches the result of communities_banned_from()
community_bans = CommunityBan.query.filter(CommunityBan.user_id == user.id).all()
return self.id in [cb.community_id for cb in community_bans]
def profile_id(self):
retval = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"

View file

@ -0,0 +1,5 @@
<a href="/topic/{{ topic.id }}/notification" rel="nofollow" aria-live="assertive"
aria-label="{{ 'Notify about new posts in this topic' if topic.notify_new_posts(current_user.id) else 'Do not notify about new posts' }}"
class="fe {{ 'fe-bell' if topic.notify_new_posts(current_user.id) else 'fe-no-bell' }} no-underline"
hx-post="/topic/{{ topic.id }}/notification" hx-trigger="click throttle:1s" hx-swap="outerHTML"
title="{{ _('Notify about every new post. Not advisable in high traffic topics!') }}"></a>

View file

@ -18,6 +18,9 @@
</ol>
</nav>
<h1 class="mt-2">{{ topic.name }}
{% if current_user.is_authenticated %}
{% include 'topic/_notification_toggle.html' %}
{% endif %}
</h1>
{% if sub_topics %}
<h5 class="mb-0" id="sub-topics">{{ _('Sub-topics') }}</h5>

View file

@ -9,9 +9,11 @@ from flask_babel import _
from sqlalchemy import text, desc, or_
from app.activitypub.signature import post_request
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_IMAGE, POST_TYPE_LINK, POST_TYPE_VIDEO
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_IMAGE, \
POST_TYPE_LINK, POST_TYPE_VIDEO, NOTIF_TOPIC
from app.inoculation import inoculation
from app.models import Topic, Community, Post, utcnow, CommunityMember, CommunityJoinRequest, User
from app.models import Topic, Community, Post, utcnow, CommunityMember, CommunityJoinRequest, User, \
NotificationSubscription
from app.topic import bp
from app import db, celery, cache
from app.topic.forms import ChooseTopicsForm
@ -206,6 +208,26 @@ def topic_create_post(topic_name):
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR)
@bp.route('/topic/<int:topic_id>/notification', methods=['GET', 'POST'])
@login_required
def topic_notification(topic_id: int):
# Toggle whether the current user is subscribed to notifications about this community's posts or not
topic = Topic.query.get_or_404(topic_id)
existing_notification = NotificationSubscription.query.filter(NotificationSubscription.entity_id == topic.id,
NotificationSubscription.user_id == current_user.id,
NotificationSubscription.type == NOTIF_TOPIC).first()
if existing_notification:
db.session.delete(existing_notification)
db.session.commit()
else: # no subscription yet, so make one
new_notification = NotificationSubscription(name=topic.name, user_id=current_user.id, entity_id=topic.id,
type=NOTIF_TOPIC)
db.session.add(new_notification)
db.session.commit()
return render_template('topic/_notification_toggle.html', topic=topic)
def topics_for_form():
topics = Topic.query.filter_by(parent_id=None).order_by(Topic.name).all()
result = []

View file

@ -331,7 +331,7 @@ def community_membership(user: User, community: Community) -> int:
@cache.memoize(timeout=86400)
def communities_banned_from(user_id) -> List[int]:
def communities_banned_from(user_id: int) -> List[int]:
community_bans = CommunityBan.query.filter(CommunityBan.user_id == user_id).all()
return [cb.community_id for cb in community_bans]
@ -710,6 +710,11 @@ def finalize_user_setup(user, application_required=False):
send_welcome_email(user, application_required)
def notification_subscribers(entity_id, entity_type) -> List[int]:
return list(db.session.execute(text('SELECT user_id FROM "notification_subscription" WHERE entity_id = :community_id AND type = :type '),
{'community_id': entity_id, 'type': entity_type}).scalars())
# topics, in a tree
def topic_tree() -> List:
topics = Topic.query.order_by(Topic.name)