Coverage for paperless/consumers.py: 26%
66 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1from __future__ import annotations
3import asyncio
4import contextlib
5import json
6from typing import TYPE_CHECKING
8from channels.generic.websocket import AsyncWebsocketConsumer
10if TYPE_CHECKING: 10 ↛ 11line 10 didn't jump to line 11 because the condition on line 10 was never true
11 from django.contrib.auth.base_user import AbstractBaseUser
12 from django.contrib.auth.models import AnonymousUser
14 from documents.plugins.helpers import DocumentsDeletedPayload
15 from documents.plugins.helpers import DocumentUpdatedPayload
16 from documents.plugins.helpers import PermissionsData
17 from documents.plugins.helpers import StatusUpdatePayload
19HEARTBEAT_INTERVAL = 30
20HEARTBEAT_MESSAGE = json.dumps({"type": "heartbeat"})
23class StatusConsumer(AsyncWebsocketConsumer):
24 heartbeat_task: asyncio.Task | None = None
26 def _authenticated(self) -> bool:
27 user: AbstractBaseUser | AnonymousUser | None = self.scope.get("user")
28 return user is not None and user.is_authenticated
30 async def _can_view(self, data: PermissionsData) -> bool:
31 user: AbstractBaseUser | AnonymousUser | None = self.scope.get("user")
32 if user is None:
33 return False
34 owner_id = data.get("owner_id")
35 users_can_view = data.get("users_can_view", [])
36 groups_can_view = data.get("groups_can_view", [])
38 if user.is_superuser or user.id == owner_id or user.id in users_can_view:
39 return True
41 return await user.groups.filter(pk__in=groups_can_view).aexists()
43 async def connect(self) -> None:
44 if not self._authenticated():
45 await self.close()
46 return
47 await self.channel_layer.group_add("status_updates", self.channel_name)
48 await self.accept()
49 self._start_heartbeat()
51 async def disconnect(self, code: int) -> None:
52 await self._stop_heartbeat()
53 await self.channel_layer.group_discard("status_updates", self.channel_name)
55 def _start_heartbeat(self) -> None:
56 self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
58 async def _stop_heartbeat(self) -> None:
59 if self.heartbeat_task is not None:
60 self.heartbeat_task.cancel()
61 with contextlib.suppress(asyncio.CancelledError):
62 await self.heartbeat_task
63 self.heartbeat_task = None
65 async def _heartbeat_loop(self) -> None:
66 while True:
67 await asyncio.sleep(HEARTBEAT_INTERVAL)
68 await self.send(HEARTBEAT_MESSAGE)
70 async def status_update(self, event: StatusUpdatePayload) -> None:
71 if not self._authenticated():
72 await self.close()
73 elif await self._can_view(event["data"]):
74 await self.send(json.dumps(event))
76 async def documents_deleted(self, event: DocumentsDeletedPayload) -> None:
77 if not self._authenticated():
78 await self.close()
79 else:
80 await self.send(json.dumps(event))
82 async def document_updated(self, event: DocumentUpdatedPayload) -> None:
83 if not self._authenticated():
84 await self.close()
85 elif await self._can_view(event["data"]):
86 await self.send(json.dumps(event))