mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
Merge remote-tracking branch 'upstream/dev' into fix/documents
This commit is contained in:
commit
c132e5ddb0
49 changed files with 1625 additions and 354 deletions
|
|
@ -5,7 +5,7 @@ Service layer for chat comments and mentions.
|
|||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy import delete, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
|
|
@ -103,6 +103,37 @@ async def process_mentions(
|
|||
return mentions_map
|
||||
|
||||
|
||||
async def get_comment_thread_participants(
|
||||
session: AsyncSession,
|
||||
parent_comment_id: int,
|
||||
exclude_user_ids: set[UUID],
|
||||
) -> list[UUID]:
|
||||
"""
|
||||
Get all unique authors in a comment thread (parent + replies), excluding specified users.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
parent_comment_id: ID of the parent comment
|
||||
exclude_user_ids: Set of user IDs to exclude (e.g., replier, mentioned users)
|
||||
|
||||
Returns:
|
||||
List of user UUIDs who have participated in the thread
|
||||
"""
|
||||
query = select(ChatComment.author_id).where(
|
||||
or_(
|
||||
ChatComment.id == parent_comment_id,
|
||||
ChatComment.parent_id == parent_comment_id,
|
||||
),
|
||||
ChatComment.author_id.isnot(None),
|
||||
)
|
||||
|
||||
if exclude_user_ids:
|
||||
query = query.where(ChatComment.author_id.notin_(list(exclude_user_ids)))
|
||||
|
||||
result = await session.execute(query.distinct())
|
||||
return [row[0] for row in result.fetchall()]
|
||||
|
||||
|
||||
async def get_comments_for_message(
|
||||
session: AsyncSession,
|
||||
message_id: int,
|
||||
|
|
@ -436,6 +467,31 @@ async def create_reply(
|
|||
search_space_id=search_space_id,
|
||||
)
|
||||
|
||||
# Notify thread participants (excluding replier and mentioned users)
|
||||
mentioned_user_ids = set(mentions_map.keys())
|
||||
exclude_ids = {user.id} | mentioned_user_ids
|
||||
participants = await get_comment_thread_participants(
|
||||
session, comment_id, exclude_ids
|
||||
)
|
||||
for participant_id in participants:
|
||||
if participant_id in mentioned_user_ids:
|
||||
continue
|
||||
await NotificationService.comment_reply.notify_comment_reply(
|
||||
session=session,
|
||||
user_id=participant_id,
|
||||
reply_id=reply.id,
|
||||
parent_comment_id=comment_id,
|
||||
message_id=parent_comment.message_id,
|
||||
thread_id=thread.id,
|
||||
thread_title=thread.title or "Untitled thread",
|
||||
author_id=str(user.id),
|
||||
author_name=author_name,
|
||||
author_avatar_url=user.avatar_url,
|
||||
author_email=user.email,
|
||||
content_preview=content_preview[:200],
|
||||
search_space_id=search_space_id,
|
||||
)
|
||||
|
||||
author = AuthorResponse(
|
||||
id=user.id,
|
||||
display_name=user.display_name,
|
||||
|
|
|
|||
|
|
@ -479,6 +479,31 @@ class VercelStreamingService:
|
|||
},
|
||||
)
|
||||
|
||||
def format_thread_title_update(self, thread_id: int, title: str) -> str:
|
||||
"""
|
||||
Format a thread title update notification (SurfSense specific).
|
||||
|
||||
This is sent after the first response in a thread to update the
|
||||
auto-generated title based on the conversation content.
|
||||
|
||||
Args:
|
||||
thread_id: The ID of the thread being updated
|
||||
title: The new title for the thread
|
||||
|
||||
Returns:
|
||||
str: SSE formatted thread title update data part
|
||||
|
||||
Example output:
|
||||
data: {"type":"data-thread-title-update","data":{"threadId":123,"title":"New Title"}}
|
||||
"""
|
||||
return self.format_data(
|
||||
"thread-title-update",
|
||||
{
|
||||
"threadId": thread_id,
|
||||
"title": title,
|
||||
},
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Error Part
|
||||
# =========================================================================
|
||||
|
|
|
|||
|
|
@ -861,6 +861,98 @@ class MentionNotificationHandler(BaseNotificationHandler):
|
|||
raise
|
||||
|
||||
|
||||
class CommentReplyNotificationHandler(BaseNotificationHandler):
|
||||
"""Handler for comment reply notifications."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("comment_reply")
|
||||
|
||||
async def find_notification_by_reply(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
reply_id: int,
|
||||
user_id: UUID,
|
||||
) -> Notification | None:
|
||||
query = select(Notification).where(
|
||||
Notification.type == self.notification_type,
|
||||
Notification.user_id == user_id,
|
||||
Notification.notification_metadata["reply_id"].astext == str(reply_id),
|
||||
)
|
||||
result = await session.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def notify_comment_reply(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
user_id: UUID,
|
||||
reply_id: int,
|
||||
parent_comment_id: int,
|
||||
message_id: int,
|
||||
thread_id: int,
|
||||
thread_title: str,
|
||||
author_id: str,
|
||||
author_name: str,
|
||||
author_avatar_url: str | None,
|
||||
author_email: str,
|
||||
content_preview: str,
|
||||
search_space_id: int,
|
||||
) -> Notification:
|
||||
existing = await self.find_notification_by_reply(session, reply_id, user_id)
|
||||
if existing:
|
||||
logger.info(
|
||||
f"Notification already exists for reply {reply_id} to user {user_id}"
|
||||
)
|
||||
return existing
|
||||
|
||||
title = f"{author_name} replied in a thread"
|
||||
message = content_preview[:100] + ("..." if len(content_preview) > 100 else "")
|
||||
|
||||
metadata = {
|
||||
"reply_id": reply_id,
|
||||
"parent_comment_id": parent_comment_id,
|
||||
"message_id": message_id,
|
||||
"thread_id": thread_id,
|
||||
"thread_title": thread_title,
|
||||
"author_id": author_id,
|
||||
"author_name": author_name,
|
||||
"author_avatar_url": author_avatar_url,
|
||||
"author_email": author_email,
|
||||
"content_preview": content_preview[:200],
|
||||
}
|
||||
|
||||
try:
|
||||
notification = Notification(
|
||||
user_id=user_id,
|
||||
search_space_id=search_space_id,
|
||||
type=self.notification_type,
|
||||
title=title,
|
||||
message=message,
|
||||
notification_metadata=metadata,
|
||||
)
|
||||
session.add(notification)
|
||||
await session.commit()
|
||||
await session.refresh(notification)
|
||||
logger.info(
|
||||
f"Created comment_reply notification {notification.id} for user {user_id}"
|
||||
)
|
||||
return notification
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
if (
|
||||
"duplicate key" in str(e).lower()
|
||||
or "unique constraint" in str(e).lower()
|
||||
):
|
||||
logger.warning(
|
||||
f"Duplicate notification for reply {reply_id} to user {user_id}"
|
||||
)
|
||||
existing = await self.find_notification_by_reply(
|
||||
session, reply_id, user_id
|
||||
)
|
||||
if existing:
|
||||
return existing
|
||||
raise
|
||||
|
||||
|
||||
class PageLimitNotificationHandler(BaseNotificationHandler):
|
||||
"""Handler for page limit exceeded notifications."""
|
||||
|
||||
|
|
@ -959,6 +1051,7 @@ class NotificationService:
|
|||
connector_indexing = ConnectorIndexingNotificationHandler()
|
||||
document_processing = DocumentProcessingNotificationHandler()
|
||||
mention = MentionNotificationHandler()
|
||||
comment_reply = CommentReplyNotificationHandler()
|
||||
page_limit = PageLimitNotificationHandler()
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -366,11 +366,14 @@ async def list_snapshots_for_thread(
|
|||
if not thread:
|
||||
raise HTTPException(status_code=404, detail="Thread not found")
|
||||
|
||||
if thread.created_by_id != user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Only the creator can view snapshots",
|
||||
)
|
||||
# Check permission to view public share links
|
||||
await check_permission(
|
||||
session,
|
||||
user,
|
||||
thread.search_space_id,
|
||||
Permission.PUBLIC_SHARING_VIEW.value,
|
||||
"You don't have permission to view public share links",
|
||||
)
|
||||
|
||||
result = await session.execute(
|
||||
select(PublicChatSnapshot)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue