Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions backend/src/baserow/contrib/database/ws/pages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any

from django.conf import settings
from django.contrib.auth.models import AnonymousUser

from baserow.contrib.database.rows.exceptions import RowDoesNotExist
from baserow.contrib.database.rows.handler import RowHandler
Expand Down Expand Up @@ -85,6 +86,20 @@ class PublicViewPageType(PageType):
type = "view"
parameters = ["slug", "token"]

def get_presence_space_name(self, slug=None, token=None, **kwargs) -> str | None:
if not slug:
return None
try:
view = ViewHandler().get_public_view_by_slug(
AnonymousUser(), slug, authorization_token=token
)
except (ViewDoesNotExist, NoAuthorizationToPubliclySharedView):
return None
return table_presence_space_name(view.table_id)

def filter_focus_for_recipient(self, page_parameters, focus, focus_type) -> bool:
return False

def can_add(self, user, web_socket_id, slug, token=None, **kwargs):
"""
The user should only have access to this page if the view exists and:
Expand Down
22 changes: 19 additions & 3 deletions backend/src/baserow/ws/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

from baserow.config.settings.utils import try_int
from baserow.ws.presence import (
ANONYMOUS_ALLOWED_PRESENCE_EVENTS,
ANONYMOUS_USER_ID,
PRESENCE_EVENT_PREFIX,
NullPresenceHandler,
PresenceHandler,
PresenceHandlerProtocol,
Expand Down Expand Up @@ -212,10 +215,15 @@ async def connect(self):
self.scope["pages"] = SubscribedPages()
web_socket_id = self.scope["web_socket_id"]
if settings.PRESENCE_VISIBLE_USERS > 0:
user_id = (
user.id
if getattr(user, "is_authenticated", False)
else ANONYMOUS_USER_ID
)
self.presence = PresenceHandler(
consumer=self,
web_socket_id=web_socket_id,
user_id=user.id,
user_id=user_id,
)
else:
self.presence = NullPresenceHandler()
Expand Down Expand Up @@ -671,9 +679,17 @@ async def broadcast_to_group(self, event: "BroadcastToChannelGroupMessage"):
payload = event["payload"]
ignore_web_socket_id = event["ignore_web_socket_id"]
exclude_user_ids = set(event.get("exclude_user_ids", None) or [])
user_id = self.scope["user"].id
user = self.scope["user"]

if not getattr(user, "is_authenticated", False):
payload_type = payload.get("type", "")
if (
payload_type.startswith(PRESENCE_EVENT_PREFIX)
and payload_type not in ANONYMOUS_ALLOWED_PRESENCE_EVENTS
):
return

if user_id in exclude_user_ids:
if getattr(user, "id", None) in exclude_user_ids:
return

if not ignore_web_socket_id or ignore_web_socket_id != web_socket_id:
Expand Down
79 changes: 67 additions & 12 deletions backend/src/baserow/ws/presence.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import uuid
from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable

from channels.db import database_sync_to_async
from loguru import logger

from baserow.core.async_redis import get_async_redis
Expand All @@ -18,6 +19,11 @@
if TYPE_CHECKING:
from baserow.ws.consumers import CoreConsumer

ANONYMOUS_USER_ID = -1
PRESENCE_EVENT_PREFIX = "presence."
# Retained for when anonymous focus emission is re-enabled (flip ANONYMOUS_FOCUS_ENABLED in web-frontend/modules/database/utils/presence.js).
ANONYMOUS_ALLOWED_PRESENCE_EVENTS = frozenset({"presence.editors_active"})

PRESENCE_KEY_PREFIX = "presence:"
PRESENCE_SPACE_TTL = 43200 # 12 hours

Expand Down Expand Up @@ -231,7 +237,7 @@ async def handle_page_subscribed(
space = None
already_in_space = False
try:
space_name = self.resolve_space_name(page_type_name, parameters)
space_name = await self.resolve_space_name(page_type_name, parameters)
if space_name is None:
return

Expand All @@ -250,16 +256,23 @@ async def handle_page_subscribed(
)
members = await self._join(space)
self._page_subscribed(page_key, space_name, page_type_name, parameters)
await self._consumer.send_json(
{
"type": "presence.members",
"space": space_name,
"entries": self._filter_members_focus(
page_type_name, parameters, members
),
}
)
if self.user_id != ANONYMOUS_USER_ID:
await self._consumer.send_json(
{
"type": "presence.members",
"space": space_name,
"entries": self._filter_members_focus(
page_type_name, parameters, members
),
}
)
else:
if self._has_auth_members(members):
await self._send_editors_active(space, active=True)
await self._broadcast_join(space)
if self.user_id != ANONYMOUS_USER_ID:
if not self._has_auth_members(members):
await self._signal_editors_active(space, active=True)
except Exception:
logger.exception("Presence subscribe failed for page {}", page_type_name)
# Roll back everything so a later re-subscribe starts clean instead
Expand Down Expand Up @@ -314,6 +327,7 @@ async def handle_page_unsubscribed(

for side_effect in (
lambda: self._broadcast_leave(space),
lambda: self._maybe_signal_last_editor_left(space),
lambda: self._consumer.send_json(
{
"type": "presence.space_discard",
Expand Down Expand Up @@ -346,6 +360,11 @@ async def handle_focus(self, page_key: str, raw_focus: dict | None) -> None:
clear focus.
"""

# Remove when enabling anonymous focus (flip ANONYMOUS_FOCUS_ENABLED
# in web-frontend/modules/database/utils/presence.js).
if self.user_id == ANONYMOUS_USER_ID:
return

space_name = self._page_to_space.get(page_key)
if space_name is None:
return
Expand All @@ -372,6 +391,7 @@ async def leave_all_spaces(self) -> None:
space = PresenceSpace(name=space_name)
await self._leave(space)
await self._broadcast_leave(space)
await self._maybe_signal_last_editor_left(space)
await self._consumer.channel_layer.group_discard(
space.channel_group, self._consumer.channel_name
)
Expand Down Expand Up @@ -490,6 +510,37 @@ async def _broadcast_focus(
},
)

@staticmethod
def _has_auth_members(members: list[ActivePresenceEntry]) -> bool:
return any(m["user_id"] != ANONYMOUS_USER_ID for m in members)

# editors_active signaling: retained for when anonymous focus emission is re-enabled (flip ANONYMOUS_FOCUS_ENABLED in web-frontend/modules/database/utils/presence.js).
async def _signal_editors_active(self, space: PresenceSpace, active: bool) -> None:
await self._broadcast(
space,
{
"type": "presence.editors_active",
"space": space.name,
"active": active,
},
)

async def _send_editors_active(self, space: PresenceSpace, active: bool) -> None:
await self._consumer.send_json(
{
"type": "presence.editors_active",
"space": space.name,
"active": active,
}
)

async def _maybe_signal_last_editor_left(self, space: PresenceSpace) -> None:
if self.user_id == ANONYMOUS_USER_ID:
return
remaining = await space.get_members()
if not self._has_auth_members(remaining):
await self._signal_editors_active(space, active=False)

def _filter_members_focus(
self,
page_type_name: str,
Expand Down Expand Up @@ -600,7 +651,9 @@ async def receive_focus_broadcast(self, event: dict) -> None:
return

@staticmethod
def resolve_space_name(page_type_name: str, parameters: dict) -> Optional[str]:
async def resolve_space_name(
page_type_name: str, parameters: dict
) -> Optional[str]:
"""
Look up the presence space name for a page type via the registry.

Expand All @@ -614,4 +667,6 @@ def resolve_space_name(page_type_name: str, parameters: dict) -> Optional[str]:
page_type = page_registry.get(page_type_name)
except page_registry.does_not_exist_exception_class:
return None
return page_type.get_presence_space_name(**parameters)
return await database_sync_to_async(page_type.get_presence_space_name)(
**parameters
)
Loading
Loading