mirror of https://gitlab.com/tildes/tildes.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
7.0 KiB
201 lines
7.0 KiB
# Copyright (c) 2018 Tildes contributors <code@tildes.net>
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""Contains the CommentNotification class."""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
|
|
from pyramid.security import Allow, DENY_ALL
|
|
from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, TIMESTAMP
|
|
from sqlalchemy.dialects.postgresql import ENUM
|
|
from sqlalchemy.orm import relationship, Session
|
|
from sqlalchemy.sql.expression import text
|
|
|
|
from tildes.enums import CommentNotificationType
|
|
from tildes.lib.markdown import LinkifyFilter
|
|
from tildes.models import DatabaseModel
|
|
from tildes.models.topic import TopicIgnore
|
|
from tildes.models.user import User
|
|
from tildes.typing import AclType
|
|
|
|
from .comment import Comment
|
|
|
|
|
|
class CommentNotification(DatabaseModel):
|
|
"""Model for a notification created by a comment.
|
|
|
|
Trigger behavior:
|
|
Incoming:
|
|
- Rows will be deleted if the relevant comment has is_deleted set to true.
|
|
Outgoing:
|
|
- Inserting, deleting, or updating is_unread will increment or decrement
|
|
num_unread_notifications for the relevant user.
|
|
"""
|
|
|
|
__tablename__ = "comment_notifications"
|
|
|
|
user_id: int = Column(
|
|
BigInteger, ForeignKey("users.user_id"), nullable=False, primary_key=True
|
|
)
|
|
comment_id: int = Column(
|
|
BigInteger, ForeignKey("comments.comment_id"), nullable=False, primary_key=True
|
|
)
|
|
notification_type: CommentNotificationType = Column(
|
|
ENUM(CommentNotificationType), nullable=False
|
|
)
|
|
created_time: datetime = Column(
|
|
TIMESTAMP(timezone=True), nullable=False, server_default=text("NOW()")
|
|
)
|
|
is_unread: bool = Column(Boolean, nullable=False, server_default="true", index=True)
|
|
|
|
user: User = relationship("User", innerjoin=True)
|
|
comment: Comment = relationship("Comment", innerjoin=True)
|
|
|
|
def __init__(
|
|
self, user: User, comment: Comment, notification_type: CommentNotificationType
|
|
):
|
|
"""Create a new notification for a user from a comment."""
|
|
if notification_type in (
|
|
CommentNotificationType.COMMENT_REPLY,
|
|
CommentNotificationType.TOPIC_REPLY,
|
|
) and not self.should_create_reply_notification(comment):
|
|
raise ValueError("That comment shouldn't create a reply notification")
|
|
|
|
self.user = user
|
|
self.comment = comment
|
|
self.notification_type = notification_type
|
|
|
|
def __acl__(self) -> AclType:
|
|
"""Pyramid security ACL."""
|
|
acl = []
|
|
acl.append((Allow, self.user_id, "mark_read"))
|
|
acl.append(DENY_ALL)
|
|
return acl
|
|
|
|
@classmethod
|
|
def should_create_reply_notification(cls, comment: Comment) -> bool:
|
|
"""Return whether a comment should generate a reply notification."""
|
|
# User is replying to their own post
|
|
if comment.parent.user == comment.user:
|
|
return False
|
|
|
|
if not comment.parent.user.is_real_user:
|
|
return False
|
|
|
|
# check if the parent's author is ignoring the topic
|
|
if (
|
|
Session.object_session(comment)
|
|
.query(TopicIgnore)
|
|
.filter(
|
|
TopicIgnore.user == comment.parent.user,
|
|
TopicIgnore.topic == comment.topic,
|
|
)
|
|
.one_or_none()
|
|
):
|
|
return False
|
|
|
|
return True
|
|
|
|
@property
|
|
def is_comment_reply(self) -> bool:
|
|
"""Return whether this is a comment reply notification."""
|
|
return self.notification_type == CommentNotificationType.COMMENT_REPLY
|
|
|
|
@property
|
|
def is_topic_reply(self) -> bool:
|
|
"""Return whether this is a topic reply notification."""
|
|
return self.notification_type == CommentNotificationType.TOPIC_REPLY
|
|
|
|
@property
|
|
def is_mention(self) -> bool:
|
|
"""Return whether this is a mention notification."""
|
|
return self.notification_type == CommentNotificationType.USER_MENTION
|
|
|
|
@classmethod
|
|
def get_mentions_for_comment(
|
|
cls, db_session: Session, comment: Comment
|
|
) -> list["CommentNotification"]:
|
|
"""Get a list of notifications for user mentions in the comment."""
|
|
notifications = []
|
|
|
|
raw_names = re.findall(LinkifyFilter.USERNAME_REFERENCE_REGEX, comment.markdown)
|
|
users_to_mention = (
|
|
db_session.query(User)
|
|
.filter(User.username.in_(raw_names)) # type: ignore
|
|
.all()
|
|
)
|
|
|
|
for user in users_to_mention:
|
|
# prevent the user from mentioning themselves
|
|
if comment.user == user:
|
|
continue
|
|
|
|
# prevent mentioning the user they're replying to
|
|
# (they'll already get a reply notification)
|
|
if comment.parent.user == user:
|
|
continue
|
|
|
|
# prevent mentioning users ignoring the topic
|
|
if (
|
|
db_session.query(TopicIgnore)
|
|
.filter(TopicIgnore.user == user, TopicIgnore.topic == comment.topic)
|
|
.one_or_none()
|
|
):
|
|
continue
|
|
|
|
mention_notification = cls(
|
|
user, comment, CommentNotificationType.USER_MENTION
|
|
)
|
|
notifications.append(mention_notification)
|
|
|
|
return notifications
|
|
|
|
@staticmethod
|
|
def prevent_duplicate_notifications(
|
|
db_session: Session,
|
|
comment: Comment,
|
|
new_notifications: list["CommentNotification"],
|
|
) -> tuple[list["CommentNotification"], list["CommentNotification"]]:
|
|
"""Filter new notifications for edited comments.
|
|
|
|
Protect against sending a notification for the same comment to the same user
|
|
twice. Edits can sent notifications to users now mentioned in the content, but
|
|
only if they weren't sent a notification for that comment before.
|
|
|
|
This method returns a tuple of lists of this class. The first item is the
|
|
notifications that were previously sent for this comment but need to be deleted
|
|
(i.e. mentioned username was edited out of the comment), and the second item is
|
|
the notifications that need to be added, as they're new.
|
|
"""
|
|
previous_notifications = (
|
|
db_session.query(CommentNotification)
|
|
.filter(
|
|
CommentNotification.comment_id == comment.comment_id,
|
|
CommentNotification.notification_type
|
|
== CommentNotificationType.USER_MENTION,
|
|
)
|
|
.all()
|
|
)
|
|
|
|
new_mention_user_ids = [
|
|
notification.user.user_id for notification in new_notifications
|
|
]
|
|
|
|
previous_mention_user_ids = [
|
|
notification.user_id for notification in previous_notifications
|
|
]
|
|
|
|
to_delete = [
|
|
notification
|
|
for notification in previous_notifications
|
|
if notification.user.user_id not in new_mention_user_ids
|
|
]
|
|
|
|
to_add = [
|
|
notification
|
|
for notification in new_notifications
|
|
if notification.user.user_id not in previous_mention_user_ids
|
|
]
|
|
|
|
return (to_delete, to_add)
|